A non-custodial wallet is a system where the user alone controls the private keys that authorize transactions; no third party can move or freeze funds. This article explains the underlying key-management model, shows how to build and test a minimal non-custodial signing workflow using standard libraries and an RPC endpoint, and provides a reproducible procedure to verify that keys never leave the client. It also covers common failure modes, trade-offs between hot and cold storage, and how to measure wallet behavior without relying on vendor claims.
Definition and scope of a non-custodial wallet
A non-custodial wallet is a software or hardware system in which the private keys controlling blockchain assets are generated and stored exclusively on infrastructure the user controls. The wallet may connect to remote nodes for reading chain state and broadcasting signed transactions, but it must never transmit the private key or seed phrase to any server. This distinguishes it from custodial wallets, where an exchange or platform holds keys on the user's behalf.
This article focuses on the technical mechanics: key derivation, local signing, RPC interaction, and verification. It does not recommend specific wallet brands or compare consumer products. For endpoint selection when connecting a wallet to a network, use the RPC Assistant, which provides short, direct recommendations for providers and networks.
- Non-custodial: user holds keys; signing happens locally.
- Custodial: platform holds keys; user authenticates to a service.
- Hybrid models (MPC, multisig) still qualify as non-custodial if no single third party can unilaterally move funds.
Prerequisites and assumptions for implementation
The examples below assume Node.js 18+ and Python 3.10+ are available, and that you have access to an RPC endpoint for an EVM-compatible network. Replace RPC_URL with your provider's endpoint. You will also need a test private key; never use a key that controls real funds in development. The code uses well-known libraries: ethers for JavaScript and web3.py for Python.
These examples are intentionally minimal to illustrate the signing boundary. Production wallets add secure key storage, user confirmation flows, transaction simulation, and hardware isolation.
npm install ethers
pip install web3Key management model: seed phrases, derivation paths, and storage
Most non-custodial wallets use a hierarchical deterministic (HD) wallet structure. A single seed phrase (typically 12 or 24 words) generates a master private key, from which child keys are derived along a derivation path such as m/44'/60'/0'/0/0 for Ethereum. The seed phrase is the ultimate recovery secret; anyone who obtains it can reconstruct all derived keys and sign transactions.
The wallet must store the seed or private key in a way that resists extraction. Common approaches include encrypted keystores (e.g., Web3 Secret Storage), operating system keychains, secure enclaves, and hardware wallets where the key never enters general-purpose memory. The critical property is that signing occurs inside a trusted boundary and the key is never serialized to a network request.
- Seed phrase = master secret; back it up offline.
- Derivation path determines which addresses are controlled.
- Encrypted keystore adds a password layer but is only as strong as the password.
- Hardware wallets keep keys in a dedicated secure element.
Transaction lifecycle in a non-custodial wallet
A transaction begins when the user initiates an action. The wallet queries the RPC endpoint for the current nonce, gas price, and chain ID, constructs an unsigned transaction object, and presents it for confirmation. Upon approval, the wallet signs the transaction hash locally using the private key. The signed raw transaction is then sent to the RPC endpoint via eth_sendRawTransaction. The node broadcasts it to the network; the wallet does not need to hold a copy of the blockchain.
This separation is fundamental: the RPC provider sees only signed transactions and public addresses, never the private key. A malicious or compromised RPC endpoint could censor or delay transactions, but it cannot forge signatures or steal funds.
const { ethers } = require('ethers');
async function signAndSend() {
const provider = new ethers.JsonRpcProvider('RPC_URL');
const wallet = new ethers.Wallet('PRIVATE_KEY', provider);
const tx = {
to: '0xRecipientAddress',
value: ethers.parseEther('0.01'),
nonce: await provider.getTransactionCount(wallet.address),
gasLimit: 21000,
gasPrice: await provider.getFeeData().then(f => f.gasPrice),
chainId: (await provider.getNetwork()).chainId
};
const signed = await wallet.signTransaction(tx);
const response = await provider.broadcastTransaction(signed);
console.log('Transaction hash:', response.hash);
}
signAndSend().catch(console.error);Runnable diagnostic: verifying local signing with a test key
The following Python script demonstrates that signing can be performed entirely offline. It creates a wallet from a test private key, signs a transaction, and recovers the sender address from the signature without contacting any RPC endpoint. If the recovered address matches the wallet address, the signing boundary is intact.
Run this in an isolated environment. The private key used here is a well-known test key; never use it for real assets.
from web3 import Web3
from eth_account import Account
# Well-known test private key (do not use for real funds)
private_key = '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318'
acct = Account.from_key(private_key)
# Build an unsigned transaction
tx = {
'nonce': 0,
'gasPrice': Web3.to_wei('10', 'gwei'),
'gas': 21000,
'to': '0x000000000000000000000000000000000000dEaD',
'value': Web3.to_wei('0.001', 'ether'),
'chainId': 1
}
signed = acct.sign_transaction(tx)
recovered = Account.recover_transaction(signed.raw_transaction)
print('Wallet address:', acct.address)
print('Recovered address:', recovered)
assert recovered == acct.address, 'Signing boundary compromised'
print('Local signing verified. No RPC call made.')Expected verification result and interpretation
When the diagnostic runs successfully, it prints the wallet address and the recovered address, which must be identical. The absence of any network call in the script confirms that signing does not depend on an RPC provider. If the addresses differ, the signing library or key derivation is misconfigured; investigate before proceeding.
For a full end-to-end test, broadcast the signed transaction to a testnet using an RPC endpoint and confirm inclusion in a block. The table below outlines the verification stages and what each proves.
- Stage 1: Offline signing and recovery — proves key isolation.
- Stage 2: Broadcast to testnet — proves RPC compatibility.
- Stage 3: On-chain confirmation — proves network acceptance.
Results table: verification checklist
Use the following table to record outcomes when testing a non-custodial wallet implementation. Do not invent values; fill in the observed results from your own environment.
| Check | Method | Expected Result | Observed |
|---|---|---|---|
| Key never leaves client | Network capture during signing | No outbound request contains private key | |
| Signature recovery | Offline recovery script | Recovered address equals wallet address | |
| RPC broadcast | Send signed tx to testnet | Transaction hash returned | |
| Confirmation | Poll eth_getTransactionReceipt | Receipt status = 0x1 | |
| Nonce management | Query eth_getTransactionCount | Nonce increments correctly |
Troubleshooting common non-custodial wallet failures
Even with correct architecture, several issues frequently arise. A transaction may be rejected with insufficient funds if the account balance does not cover value plus gas; this is a chain-state issue, not a signing failure. A nonce too low error indicates a pending transaction with the same nonce; query the pending nonce using eth_getTransactionCount with the pending block tag. If the RPC endpoint returns method not found, the provider may not support the required JSON-RPC method; switch to a compatible endpoint.
Signature verification failures often stem from mismatched chain IDs or EIP-1559 versus legacy transaction formats. Ensure the chainId in the signed transaction matches the network. For hardware wallets, confirm the device is unlocked and the correct derivation path is selected.
- Insufficient funds: check balance and gas estimate.
- Nonce too low: use pending nonce or wait for confirmation.
- Chain ID mismatch: verify network configuration.
- RPC method unsupported: use a provider with full method coverage.
Trade-offs: hot wallets, cold storage, and operational risk
Non-custodial wallets shift responsibility to the user. Hot wallets (software connected to the internet) offer convenience but expose keys to malware and phishing. Cold wallets (hardware or air-gapped) reduce online attack surface but complicate transaction workflows. Multisignature and MPC wallets distribute trust among multiple parties or devices, eliminating single points of failure at the cost of coordination overhead.
There is no universally correct choice; the appropriate model depends on asset value, transaction frequency, and threat model. For high-value holdings, hardware isolation and geographic redundancy of seed backups are standard. For frequent small transactions, a hot wallet with strict operational hygiene may be acceptable.
Next steps: integrating with infrastructure and measuring reliability
Once local signing is verified, connect the wallet to reliable RPC infrastructure. OnFinality provides managed RPC endpoints across many networks; review the supported networks and API service options. For high-throughput or latency-sensitive applications, consider dedicated nodes. Pricing details are available for capacity planning.
To measure reliability, run a script that periodically queries eth_blockNumber and records latency and error rates over a defined window. Compare providers using identical methodology. For concrete endpoint recommendations, consult the RPC Assistant rather than relying on generic lists.
- Review supported networks: /en/networks
- Explore API service tiers: /en/api-service
- Compare dedicated node options: /en/dedicated-node
- Check RPC pricing: /en/pricing/rpc
- Learn more about RPC fundamentals: /en/learn