Architecture

Deposit address derivation

Each BabyFX Pay invoice gets a unique, un-reused deposit address. The address is derived deterministically from a master mnemonic via the standard Ethereum HD path.

Visual path

Path

m / 44'' / 60'' / 0'' / 0 / <index>
  • 44'' is the BIP-44 purpose.
  • 60'' is the SLIP-0044 coin type for Ethereum and EVM-compatible chains.
  • The first three hardened levels land on the same root as any standard Ethereum wallet derived from the same seed.
  • The fourth level is the account; the fifth is the invoice index.

The derivation index is allocated by a Postgres sequence (invoice_deriv_seq, see pay/schema.sql) so concurrent invoice creation can never produce a collision. The UNIQUE constraint on invoices.derivation_index is the second line of defense.

Hardened keys are denoted by the apostrophe ('). They cannot derive child public keys from a parent public key alone — you need the parent private key. This is what makes the mnemonic the only thing you need to back up.

Code

The derivation is a few lines in pay/lib.js:

import { ethers } from "ethers";
const hdRoot = ethers.HDNodeWallet.fromPhrase(PAY_MNEMONIC, "", "m");

export function deriveDepositAddress(index) {
  const child = hdRoot.derivePath(`m/44''/60''/0''/0/${index}`);
  return ethers.getAddress(child.address);
}

The same root is also used by deriveDepositWallet(index, provider) in sweep.js, which returns a Wallet capable of signing the sweep transaction after an invoice is confirmed.

Why HD and not a keytree

  • One mnemonic in one place; everything else is a function of index.
  • Recovery is trivial: given the mnemonic and the sequence, every invoice address is reproducible.
  • The index allocator is a single SQL sequence, so there is no need to scan a keytree to find the next free index.

Key custody

The master mnemonic must not be in environment variables in production. The local dev stack uses PAY_MNEMONIC as a convenience; the comment in pay/lib.js says so explicitly:

> DEV ONLY master mnemonic for deriving per-invoice deposit addresses. In production this lives in an HSM / KMS, never in env.

For public testnet, the team needs to wire the derivation root into an HSM or KMS that can sign eth_sendTransaction calls without ever returning the private key to the application server. The same is true of the merchant''s settlement wallet private keys (used by the sweeper).

Index allocator

CREATE SEQUENCE IF NOT EXISTS invoice_deriv_seq START 1;

The index is consumed inside the same transaction that inserts the invoice (pay/index.js, POST /v1/invoices), so a crash mid-request never produces a "spent" index.