Security & Key Management
The WaaS security model is built around industry-standard HD wallet cryptography with layered API permissions for sensitive operations.
HD Wallet Architecture
Each customer wallet is a BIP39 HD wallet:
- BIP39 — Mnemonic phrase (12 or 24 words) is the wallet root
- BIP32 — Hierarchical deterministic key tree derived from the mnemonic
- BIP44 — Standardised derivation paths per network (
m/44'/coin'/account'/change/index)
A single mnemonic deterministically generates all addresses across all networks. This means:
- One backup recovers all addresses and funds
- Addresses are derived offline — no network call needed
- The same customer ID always produces the same wallet root
Key Storage
- Mnemonics are encrypted at rest using AES-256-GCM
- Private keys are derived on-demand from the encrypted mnemonic — they are not stored separately
- The mnemonic is returned only once at wallet creation
- After creation, retrieval requires the
wallet:reveal_seedAPI permission
API Key Permissions
Sensitive WaaS operations require explicit permissions on the API key. Grant only what your integration needs.
| Permission | Required For |
|---|---|
| (none / default) | Creating wallets, deriving addresses, listing addresses, balance queries |
wallet:transfer | POST /wallet/{id}/transfer — sending crypto |
wallet:reveal_seed | GET /wallet/{id}/seed — retrieving the mnemonic |
wallet:export_key | POST /wallet/{id}/export-key — exporting raw private keys |
Configure permissions in your API key settings in the dashboard.
Secure Mnemonic Handling
When you create a customer wallet, the mnemonic is returned in the response. You must:
- Store it immediately in a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, etc.)
- Never log it — disable request/response logging for the wallet creation endpoint
- Encrypt in transit — always use HTTPS
- Never expose to the frontend — this is a server-side operation only
- Rotate access — revoke API keys with
wallet:reveal_seedpermission if compromised
// GOOD — store before doing anything else
const wallet = await rach.wallet.create({ customerId: userId });
await secretsManager.put(`wallet/${userId}/mnemonic`, wallet.mnemonic);
// BAD
console.log('Wallet created:', wallet); // ← logs mnemonic
res.json(wallet); // ← exposes mnemonic to frontendPrivate Key Export
The POST /wallet/{id}/export-key endpoint is intended for customer-initiated wallet exports (e.g., when a customer wants to import their wallet into MetaMask or another external wallet).
Best practices:
- Require re-authentication or 2FA before exporting
- Only export when the customer explicitly requests it
- Never store the exported private key — it's shown once
- Log the export event for audit purposes (but not the key itself)
- Do not implement auto-export or background key access
Deposit Security — Flash Loans, Fake Transactions & Testnet Isolation
Flash Loans Cannot Trigger Deposit Webhooks
Flash loans borrow and repay within a single atomic on-chain transaction. The net balance change on your monitored address is zero. Rach's monitor detects deposits by comparing currentBalance - previousBalance on each poll cycle. Because a flash loan leaves no net balance change, it is invisible to the monitor and will never fire a webhook.
Testnet Transactions Cannot Affect Mainnet
Testnet and mainnet are separate blockchains with entirely separate state. A transaction on a test network cannot change the balance of a mainnet address. Rach enforces this at the infrastructure level: test_sk_* API keys query testnet RPCs, live_sk_* keys query mainnet RPCs. There is no possible cross-contamination.
EVM 0-Confirmation Safety
On EVM chains (ETH, BSC, POL), eth_getBalance returns the confirmed on-chain balance only — pending mempool transactions are not reflected. By the time Rach detects a balance increase, the transaction is already included in a mined block. A "confirmations": 0 value in wallet.deposit.detected means the deposit is confirmed on-chain but block depth has not yet been measured — it is not a mempool transaction.
The safe_to_credit Field Is Your Credit Gate
Every deposit webhook payload contains an explicit safe_to_credit boolean:
wallet.deposit.detected→"safe_to_credit": false— show pending status onlywallet.deposit.confirmed→"safe_to_credit": true— safe to credit customer
This field accounts for all networks, all confirmation methods, and all edge cases. Your integration should only credit when safe_to_credit === true, regardless of the confirmations count or status string.
// Correct
if (event === 'wallet.deposit.confirmed' && data.safe_to_credit === true) {
await wallet.credit(data.customer_id, data.amount, data.currency);
}
// Dangerous — do not do this
if (event === 'wallet.deposit.detected') {
await wallet.credit(data.customer_id, data.amount, data.currency); // ❌
}Webhook Signature Verification
Always verify the X-Webhook-Signature header on every incoming webhook before processing:
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // must be raw Buffer — not parsed JSON
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}Testnet vs Mainnet Isolation
- Test keys (
test_sk_*) always derive testnet addresses — they cannot interact with mainnet - Live keys (
live_sk_*) always derive mainnet addresses - A session/address created with one key type cannot be accessed with the other
- Never use live keys in development or CI environments
Rate Limiting & Abuse Prevention
- All API endpoints are rate-limited per API key
- The
wallet:transferendpoint has stricter limits than read-only operations - Implement your own rate limiting on customer-facing operations before they reach the API
Compliance Considerations
- Rach is not a custodian — you are responsible for the wallets created via WaaS
- Implement your own AML/KYC checks before allowing large deposits or withdrawals
- Maintain your own audit trail of all
wallet.deposit.confirmedevents - Consider jurisdiction-specific regulations on custodying crypto assets on behalf of customers
