Wallet Security
Nexgent uses a non-custodial model where private keys remain on your infrastructure. This page covers how wallets are handled and best practices for securing them.
Critical: Private keys control real funds. A compromised key means total loss of funds with no recovery possible.
Architecture
┌─────────────────────────────────────────────────┐
│ Your Infrastructure │
│ ┌───────────────────────────────────────────┐ │
│ │ Environment Variables │ │
│ │ WALLET_1=<base58_private_key> │ │
│ │ WALLET_2=<base58_private_key> │ │
│ └─────────────────────┬─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ Wallet Loader (Startup) │ │
│ │ Decode base58 → Keypair │ │
│ └─────────────────────┬─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ In-Memory Wallet Store │ │
│ │ Map<publicKey, Keypair> │ │
│ │ (Never persisted to disk/database) │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘Key Principles
1. Keys Never Leave Memory
Private keys are:
- Loaded once at startup
- Stored only in memory (Map)
- Never written to database
- Never logged
- Never sent over network
// Wallet store - memory only
class WalletStore {
private wallets: Map<string, Keypair> = new Map();
getKeypair(address: string): Keypair | undefined {
return this.wallets.get(address);
}
}2. Keys Never in Database
The database stores only public wallet addresses:
model AgentWallet {
id String @id
walletAddress String @unique // Public address only
agentId String
}3. Keys Never Logged
// NEVER do this
console.log(keypair.secretKey); // ❌
// Safe: log public key only
console.log(`Loaded wallet: ${publicKey.slice(0, 8)}...`); // ✓Configuration
Environment Variables
# Wallet private keys (base58 encoded)
# Export from Phantom: Settings > Security > Export Private Key
WALLET_1=your-base58-private-key-here
WALLET_2=another-private-key-hereWallets are only needed for live trading mode. Simulation mode doesn't require private keys.
Loading Process
// infrastructure/wallets/wallet-loader.ts
export class WalletLoader {
loadWallets() {
const wallets = new Map<string, Keypair>();
for (let i = 1; i <= 10; i++) {
const privateKey = process.env[`WALLET_${i}`];
if (privateKey) {
// Decode base58 to bytes
const secretKey = bs58.decode(privateKey);
// Create keypair
const keypair = Keypair.fromSecretKey(secretKey);
// Store by public key
wallets.set(keypair.publicKey.toBase58(), keypair);
}
}
return wallets;
}
}Best Practices
Use Dedicated Trading Wallets
Don't use your main wallet. Create separate wallets specifically for trading:
- Generate a new wallet in Phantom/Solflare
- Transfer only trading funds
- Keep main holdings separate
Minimize Wallet Balance
Only keep funds you're actively trading:
| Risk Level | Recommendation |
|---|---|
| Low | Keep 1-2 SOL for testing |
| Medium | Keep 5-10 SOL for active trading |
| High | Never keep more than you can afford to lose |
Keep Main Wallet Separate
Most users use browser wallets like Phantom or Solflare:
- Create a separate wallet specifically for Nexgent trading
- Keep your main holdings in your primary wallet
- Transfer only trading funds to the dedicated trading wallet
- Never use your primary wallet's private key with Nexgent
Rotate Wallets Periodically
- Create new trading wallet
- Transfer funds from old wallet
- Update
WALLET_Nenvironment variable - Restart backend
- Reassign agents to new wallet
Simulation Mode
For testing without risk, use simulation mode:
// Agents in simulation mode use virtual wallets
const agent = await createAgent({
name: 'Test Agent',
tradingMode: 'simulation', // No real wallet needed
walletAddress: 'sim_virtual_wallet',
});Simulation mode:
- Doesn't require private keys
- Uses virtual balances
- Simulates trades without on-chain execution
- Perfect for testing strategies
Transaction Signing
When executing live trades:
// 1. Get quote from Jupiter (unsigned transaction)
const quote = await jupiterProvider.getQuote({
inputMint: SOL_MINT,
outputMint: tokenAddress,
amount: amountLamports,
walletAddress: walletAddress,
});
// 2. Get keypair from memory
const keypair = walletStore.getKeypair(walletAddress);
// 3. Sign transaction locally
const transaction = VersionedTransaction.deserialize(
Buffer.from(quote.transaction, 'base64')
);
transaction.sign([keypair]);
// 4. Submit signed transaction
const result = await jupiterProvider.submitTransaction(transaction);The private key is used only for signing and never leaves the server.
Security Checklist
Setup
- Use dedicated trading wallets (not main wallet)
- Keep minimal balance in trading wallets
- Store private keys in environment variables
- Never commit
.envfiles to git - Use
.gitignorefor all env files
Operations
- Monitor wallet balances regularly
- Review transactions for unexpected activity
- Rotate trading wallets periodically
- Use simulation mode for testing
Infrastructure
- Restrict server access (SSH keys, no passwords)
- Use secrets manager in production
- Enable audit logging
- Set up balance alerts
Emergency Procedures
Suspected Compromise
- Immediately transfer funds to a new wallet
- Revoke the old wallet from all agents
- Update environment variables
- Restart the backend
- Investigate the breach
Lost Private Key
If you lose access to a private key:
- Funds in that wallet are permanently inaccessible
- Always keep backups in a secure location
- Consider using a password manager for key backup