Milestone 5.2c — HD wallet deposit addresses
Status: Shipped on main (env-gated) — api 5b40b8b, web 97436ab, UX fix 8aa2197
Tracking: ehx-api#13 (closed)
Parent: ehx-web#4 checkout epic
Why HD wallet?
| Mode | Address | Reconciliation |
|---|---|---|
| stub_address | SHA-256 of session id | No private key — demo only |
| custodial | One treasury per chain | All payments share one address — must attribute by amount/time |
| hd_wallet | Unique per session (m/44'/60'/0'/0/n) | Payment to address n = session n — production-ready attribution |
HD (hierarchical deterministic) derivation lets one mnemonic (or xprv) in secure storage generate unlimited deposit addresses without storing each private key in the database. Only the derivation index is stored on the session row.
Configuration
# Enable HD mode (takes precedence over stub; custodial env still wins if set)
EHX_CHECKOUT_HD_ENABLED=1
# 12 or 24-word BIP39 mnemonic — NEVER commit; use secret manager in production
EHX_CHECKOUT_HD_MNEMONIC="word1 word2 ... word12"
Priority when creating a session:
EHX_CHECKOUT_DEPOSIT_*treasury →settlement_mode: custodial- HD enabled + mnemonic →
settlement_mode: hd_wallet+deposit_derivation_index - Else →
settlement_mode: stub_address
Operator runbook
- Generate a dedicated mnemonic (or xprv) for checkout deposits only — not your personal wallet.
- Store in
.env/ secret manager; rotate by migrating to a new mnemonic and new index range (plan sweep first). - Sweep: use the same mnemonic in an offline or HSM tool with path
m/44'/60'/0'/0/{deposit_derivation_index}to move funds to cold treasury. - RPC verification treats
hd_walletlike custodial (recipient + amount checks).
API fields
| Field | Meaning |
|---|---|
settlement_mode | hd_wallet when derived |
deposit_derivation_index | Integer index for BIP44 path (operator reference) |
deposit_address | Derived EVM address (same on Ethereum, Base, Arbitrum) |
Multi-chain (one mnemonic)
You do not need separate wallets per chain. One EHX_CHECKOUT_HD_MNEMONIC derives the same EVM address for each session index; the buyer selects Ethereum, Base, or Arbitrum One at checkout and sends the quoted asset on that network to that address.
Docker Compose (ehxlabs.xyz)
Add to ehx-web/.env (loaded by the api service via env_file):
EHX_CHECKOUT_HD_ENABLED=1
EHX_CHECKOUT_HD_MNEMONIC="…" # secret manager in production; never commit
Then recreate the API: docker compose up -d --force-recreate api.
Leave EHX_CHECKOUT_DEPOSIT_* unset unless you intentionally want custodial treasury mode (it overrides HD).
Checkout UI (web 8aa2197)
- Do not show the stub “demo address” callout while
?session=is loading from the API. - After load, show Unique HD deposit address when
settlement_modeishd_wallet(each session may use a different BIP44 index — not “wrong”, expected).
Still open on this milestone
- Automated sweep / forwarding to cold treasury (operator tooling)
- Production mnemonic rotation runbook beyond manual index migration
Security
- Mnemonic must never appear in logs, Git, or analytics events.
- Restrict who can read production
.env. - Test/dev: use the public Hardhat mnemonic only on testnets.