Skip to main content

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?

ModeAddressReconciliation
stub_addressSHA-256 of session idNo private key — demo only
custodialOne treasury per chainAll payments share one address — must attribute by amount/time
hd_walletUnique 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:

  1. EHX_CHECKOUT_DEPOSIT_* treasury → settlement_mode: custodial
  2. HD enabled + mnemonic → settlement_mode: hd_wallet + deposit_derivation_index
  3. Else → settlement_mode: stub_address

Operator runbook

  1. Generate a dedicated mnemonic (or xprv) for checkout deposits only — not your personal wallet.
  2. Store in .env / secret manager; rotate by migrating to a new mnemonic and new index range (plan sweep first).
  3. 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.
  4. RPC verification treats hd_wallet like custodial (recipient + amount checks).

API fields

FieldMeaning
settlement_modehd_wallet when derived
deposit_derivation_indexInteger index for BIP44 path (operator reference)
deposit_addressDerived 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_mode is hd_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.