# ADR 004 — Custodial NFT sale-lock candidate Status: **offline executable candidate; NOT activated or independently reviewed**. Evidence: 2026-10-20. Related: [production plan](production-plan.md), [custodial model](custodial-market-design.md), [PostgreSQL storage](postgres-financial-storage.md). ## Purpose and activation boundary Specify the missing asset-control rules before enabling real custodial purchases. An ordinary signed NFT transfer can survive a website refund deadline; balance reservations alone cannot prevent that late delivery. This candidate couples sale authorization to explicit canonical locks and terminal outcomes. [Signed schemas/messages](../shared/sale-candidate.ts) and [deterministic state machine](../server/testnet/sale-candidate.ts) are deliberately separate from active consensus. The existing transaction schema rejects all candidate transactions. The candidate domain derives from the existing chain identity, explicit policy and candidate version; it is an offline signature domain, **not a reviewed network manifest/genesis**. No current chain identity, miner, peer endpoint or custody signing capability is changed. A separately gated [disposable PostgreSQL accounting rehearsal](custody-sale-rehearsal.md) now changes fictitious reservations using signed synthetic transcripts; it is not mined-chain integration. The pure `termsForCustodyOrder()` bridge drafts terms from an actual awaiting-lock reservation and checks exact seller/author/host allocations. It does not sign, persist, relay or debit anything. Tests exercise this bridge against the current custody engine. Public custody orders continue stopping at `awaiting-lock`. ## Signed authorization Terms bind candidate protocol, network, chain/policy digest, custodian/order IDs, exact card/sequence, seller, buyer, host signing identity, native-XMR gross, hosting rate, fixed author policy, lock-admission deadline and final expiry height. Independent downward commission rounding remains unchanged. No production threshold is invented: maturity/depth/window limits are explicit policy inputs committed by the candidate digest. Optional `hostFeeAddress` appends a tagged hosting-recipient tuple, preserving historical candidate signature bytes when omitted. Drafting preserves the reserved order's snapshot; the PostgreSQL rehearsal additionally checks the exact listing rate, not just rounded allocation equality. The candidate performs address-shape validation only, with checksum/network validation supplied by custody admission and business audits. This extension is still unactivated and requires the same protocol review as the candidate. - **Lock:** both seller and buyer sign the complete terms. Host signing identity must differ from both trading identities. Distinct keys are role separation, not proof of independent humans/operators. - **Completion:** the terms' exact host signs the sale ID plus a reservation digest, attesting that sufficiently confirmed/unlocked funds were reserved under the approved policy. Host cannot select another card/recipient/price. Validators would verify the host's authorization, **not Monero truth**. The reservation digest is an accounting commitment, not a cryptographic proof of backing or solvency. - **Cancellation:** host and buyer sign the sale ID. Neither can unilaterally cancel early. Seller already consented to this branch in signed lock terms; seller retains card ownership on cancellation. - **Expiry:** permissionless explicit transaction, without a private key, once the canonical expiry height is reached. Website clocks/RPC errors never cause implicit release. ## State transitions and exact heights Let $h$ be the block executing a transaction, $L$ the accepted lock height, $A$ the last lock-admission height, $E$ the expiry height, $d_L$ the lock-depth policy and $d_O$ the outcome-depth policy. | Transition | Validity / result | | --- | --- | | Unlocked → locked | Both signatures, mature current seller ownership/sequence, no existing lock, unused custodian/order; $h\le A$, $A+d_L