# ADR 002 — Custodial XMR balances and marketplace settlement Status: **implementation authorized; sandbox accounting foundation implemented, live custody disabled**. Updated 2026-10-20. Supersedes the preferred research direction in [ADR 001](settlement-feasibility.md), not its evidence or security warnings. Related: [production plan](production-plan.md). ## 1. Recommendation and scope Use one accountable custodian per market, a pooled Monero treasury with deposit subaddresses, and an internal double-entry XMR ledger. Purchases move liabilities between ledger accounts; only deposits, withdrawals and treasury movements touch Monero. Start with fixed-price sales, one authoritative financial writer and no cross-host credit, user-to-user balance transfers, lending or live auctions. The operator has authorized proceeding with implementation instead of the previously preferred arbitrated multisig route. The first increment below is an isolated sandbox; no wallets, payable addresses, live deposits, keys or funds were accessed. Production treasury infrastructure and new NFT consensus rules are not activated. Current mock v3 terms and historical v2 orders remain unchanged. ### Implemented first increment - [Custody command domain](../shared/custody.ts): strict versioned sandbox consent, exact atomic amounts, custodian/network/actor/expiry/nonce binding and fixed author policy. Existing market signatures cannot debit custody balances. - [Custody engine](../server/market/custody.ts): isolated testnet SQLite/WAL/FULL-sync financial model, transactional double-entry postings, immutable-entry triggers, startup replay audits, durable receipts, one-time fictitious credits, exclusive fixed-price reservations, blocked outbox and cancellation of unstarted intents. Resource budgets stop new work without blocking release of existing holds. PostgreSQL remains the planned production store; this SQLite implementation is not approved for real custody. - [Custody UI](../src/CustodyPanel.tsx): marketplace “Custody preview” tab with reviewed signatures, private/paged balances and activity, test funding, listing/reservation and withdrawal-hold cancellation. A lost response retains the original signed envelope for replay rather than creating a new debit nonce. - HTTP endpoints `/api/custody` and `/api/custody/command` are off by default; explicit `WIKIDECK_CUSTODY_MODE=sandbox` enables testnet only. All other enabled modes and mainnet sandbox are rejected before node resources open. Requests use the node's origin/admission/body limits, per-route mutation throttling and sanitized errors. No observer/admin settlement or balance-credit endpoint exists. - [Stopped-node recovery](../server/recovery.ts) includes the optional custody sandbox database and its identity while continuing to accept existing two-database backups. Financial receipts are not pruned. Restore/startup checks do not substitute for production reconciliation. - Offline SQLite fixture methods exercise fee allocation and prepared-withdrawal identity/retry/cancellation boundaries. They never access Monero and are not callable through HTTP. Public reservations stop at `awaiting-lock`; there is **no automatic NFT delivery or settled sale** in this increment. No real-address withdrawal, deposit subaddress, wallet RPC, signer, author payout or reorg compensation is implemented yet. **Financial storage update:** [ADR 003](postgres-financial-storage.md) selects PostgreSQL and records the integrated asynchronous custody backend, restricted runtime/schema roles, deferred accounting constraints, explicit SQLite freeze/import, and full dump/restore rehearsal. SQLite remains an opt-in-compatible local sandbox default, not an automatic fallback. Real custody and production operating acceptance remain blocked. Validation: TypeScript/Vite build and 144 offline tests passed in the serial native-RandomX test run; the initial parallel run encountered a peer-gossip timeout. Browser smoke verified a disposable wallet's signed test credit, withdrawal hold and cancellation restoring the exact balance, plus a 390px viewport without horizontal overflow. No real funds were used. Tests and remaining acceptance gates are below; this is not launch readiness. **Security claim:** operator-custodied XMR, seller-authorized and host-attested NFT sale completion, with durable accounting and recoverable coordination. Not trustless, not an atomic Monero/NFT swap, not insured, and not an independently consensus-verified Monero payment. The operator can steal, censor, become insolvent or lose keys; segregation and review reduce but do not remove that risk. ## 2. Identity and trust boundaries - A balance belongs to `(custodianId, Wikideck network, chainId, Wikideck public-key identity, Monero network)`. Identical user keys at different hosts do not create shared balances. The UI must name the custodian and networks before any deposit. - Allocate a deposit subaddress lazily after authenticated proof of Wikideck key possession and custody-term acceptance. Store wallet generation/ID, account index, subaddress index, address and immutable beneficiary mapping **before** displaying it. Never reassign an old address; rotated addresses remain monitored. Bound allocations to prevent lookahead/storage abuse. - Subaddresses identify incoming outputs; wallet RPC can report output balances per subaddress, but these are **not the marketplace entitlement ledger**. After internal sales or pooled withdrawals those figures no longer represent what a customer is owed. There is no on-chain subaddress-to-subaddress purchase transfer. - Keep Wikideck wallet identity keys client-held. Monero spending keys are operator-held, separate from the market attestation key, admin credentials and application deployment credentials. A deposit proof does not authenticate its sender; deposits credit the preassigned beneficiary, not an arbitrary claimant supplying a txid. - Lost Wikideck keys are not recovered by publishing a Monero proof. Recommend an optional separately enrolled recovery credential and delayed, audited dual-control recovery; absent an approved recovery policy, no unilateral support reassignment. Withdrawal authorization requires fresh key proof and an independently enrolled second factor. Browser key storage/session security must be reviewed before custody. ## 3. Components and storage | Component | Responsibility / access | | --- | --- | | Market API | Validates signed, versioned intents; presents balances, deposit details and receipts. No treasury spending key or generic wallet RPC proxy. | | Ledger and order coordinator | Sole authoritative transactional writer for reservations, entries, order transitions, inbox/outbox and receipt/idempotency records. | | Deposit observer | Private wallet scanner and owned Monero daemon; observes output identity, canonical inclusion and unlock state. Prefer view-only operation; spent-state reconciliation requires current signed key images or another reviewed spend-aware source. | | Wikideck observer / relayer | Tracks canonical lock, completion, cancellation and reorg events using durable cursors; retries exact stored messages. Cannot edit financial balances. | | Treasury worker / isolated signer | Serializes spend construction, validates approved withdrawal intents, destination, amount, fee cap and policy; persists prepared transactions before relay. Independent limits and approvals, not blind signing for API requests. | | Reconciler and operations | Independently compares ledger, wallet and chain observations; alerts/halts affected operations; administers reviewed compensations, not arbitrary balance edits. | PostgreSQL is now selected and integrated for the custody sandbox under [ADR 003](postgres-financial-storage.md): exact `NUMERIC` with explicit integer checks (avoiding silent scale rounding), transactional row/advisory locking, unique constraints and separately reviewed backup/PITR operations. SQLite remains suitable for disposable compatibility models; no automatic promotion/fallback occurs. Financial order transitions and journal entries share one PostgreSQL transaction; external chain/wallet effects require a durable outbox, never a cross-database pretend transaction. One financial writer at a time initially; readers use coherent snapshots. Future treasury failover must fence the old worker before spending resumes. Minimal entities: custodians/network bindings, authenticated customer identities/recovery enrollment, deposit address mappings, observed outputs and inclusion history, ledger accounts/journals/entries, immutable sale terms, purchase reservations, chain events/cursors, withdrawal intents/attempts/prepared transactions, outbox/inbox, signed receipts and audited cases/approvals. Mutable projections are rebuildable; history and external-effect identity are not discarded. Use canonical atomic-unit strings in APIs and signatures and `bigint` internally; never parse RPC monetary JSON through JavaScript `Number`. Pin and test an exact-integer RPC codec. Individual amounts and aggregate balances need explicit bounds; aggregate liabilities must not inherit a per-order bound accidentally. ## 4. Accounting model and invariants Customer balances are liabilities, partitioned into `available`, `purchase-reserved`, `sale-pending` and `withdrawal-reserved`. Show uncredited deposit observations separately. Keep author commission liabilities, operating revenue, treasury assets, withdrawal-in-flight clearing, network expense and operator capital distinct. Pending credits are not spendable. Example postings (debits/credits follow normal asset/liability conventions): | Event | Debit | Credit | | --- | --- | --- | | Eligible deposit | Treasury XMR asset | Customer available liability | | Purchase reservation | Buyer available liability | Buyer order-reserved liability | | Mature canonical sale | Buyer order-reserved liability | Seller available liability + author payable liability + earned host commission | | Safe cancellation | Buyer order-reserved liability | Buyer available liability | | Withdrawal reserve | Customer available liability | Customer withdrawal-reserved liability | | Confirmed withdrawal | Customer withdrawal-reserved liability | Treasury XMR asset (recipient amount + approved actual network fee) | For the withdrawal row, reserve recipient amount plus a signed maximum fee first, release unused fee allowance in a balanced posting, and retain an in-flight asset/liability reconciliation bridge between relay and confirmation. A treasury movement between operator wallets is an asset reclassification, not a customer deposit. The detailed chart of accounts, fee clearing and accounting recognition timing require review before implementation. Mandatory invariants: - Every posted journal balances exactly in atomic XMR; journals are immutable. Corrections are linked compensating entries with reason and approval. - No spendable customer account goes negative; reservation checks, posting and order state change commit together. Concurrent purchases/withdrawals cannot spend the same available balance. - Every order reserves at most one gross and settles or cancels once. A nonce reused with different content is rejected; retries return the original receipt. - Count each economic deposit once using wallet/network plus transaction/output identity (not txid alone or a reorg-sensitive global index). Pin the supported RPC representation and output-key/index mapping in fixtures; aggregated reports must not duplicate individual outputs. Spending the pooled output later does not erase its customer's credited liability. - Maintain at least 100% backing of recognized customer/author liabilities, plus separately funded operating/fee/loss capital. Reconcile assets and liabilities on the same snapshot with in-flight withdrawals, locked change and internal treasury transfers accounted exactly once. Locked assets can provide backing but not immediate withdrawal liquidity. - Never borrow customer funds to fund expenses, reserves or promised compensation. Revenue withdrawal is blocked if backing/liquidity limits fail. Reserve proofs alone do not prove liabilities or solvency. ## 5. Deposit lifecycle `observed → confirming/locked → available`, or `orphaned / quarantined`. Scan authoritative wallet-owned outputs, not buyer screenshots, tx proofs or callbacks. Require healthy daemon/wallet synchronization, accepted canonical inclusion, a versioned confirmation threshold and actual wallet-reported unlock eligibility. No mempool credit; no wall-clock-only maturity. Persist inclusion block hashes and rescan overlapping ranges after restarts/reorgs; maintain wallet allocation lookahead from backed-up mappings. Unknown deposits go to a suspense queue, not operating revenue. Multiple/partial deposits simply accumulate when individually eligible; deposits are not invoices and do not reserve a listing. Credit the actual valid amount, with no conversion or hidden deposit fee in the proposed baseline. Tiny output handling must be resource-bounded without silently confiscating legitimate funds. Disabling new deposit displays cannot stop payments to previously issued addresses: continue monitoring and handling those liabilities. If a previously credited deposit is orphaned, append a reversal/restricted-balance case and freeze affected spending. Do not silently debit unrelated customers or pretend a completed withdrawal can be undone. Record any already-consumed shortfall as an operator loss/receivable under approved policy, with incident exposure capped by funded capital. Deep reorg risk is residual, not eliminated by confirmations. ## 6. Fixed-price sale lifecycle and NFT coordination Proposed order states: `funds-reserved → lock-pending → locked → completion-submitted → delivery-confirming → settled`; alternatives: `cancel-pending → cancelled`, or `review` at any uncertain boundary. Seller proceeds are not spendable before `settled`. 1. Seller signs fresh custody-protocol listing terms: exact card/sequence, allowed host settlement key, exact XMR gross, fee split/beneficiaries, protocol/network/order domain and validity rules. Buyer reviews and signs the immutable checkout quote, including the custodian, recipient identity, maturity and cancellation policy. V3 mock signatures do not authorize debiting real balances; use a new domain/version with explicit consent. 2. In one financial transaction, validate terms/availability, reserve funded **available** buyer balance, allocate an order ID and store the lock outbox intent. A local reservation protects buyer funds, not the NFT. If the seller has already transferred the card elsewhere, fail safely and release only after no live completion authority remains. 3. A new, separately reviewed Wikideck sale-lock transaction binds card/sequence, seller, buyer, order/market/chain IDs, terms digest, host public key and consensus-height deadlines. Ownership remains with the seller, but conflicting transfers and locks are rejected. Obtain the seller's exact authorization; the host must not be able to redirect the card to itself or another buyer. 4. After lock maturity and a durable reservation, the host signs a narrowly scoped funding/release attestation and relays the matching sale completion. Validators verify deterministic lock/signature/deadline rules, **not** Monero RPC or internal balances. They trust the host's assertion that customer funds were reserved under its deposit policy. A compromised host can lie about funding; seller assent and host accountability are the remaining protections. 5. On canonical completion at the selected Wikideck depth, commit once: consume buyer reservation and allocate seller, author and host amounts. The outbox/inbox and unique completion identity recover a crash after chain delivery but before journal commit. No Monero transaction occurs for this purchase. Display inclusion and maturity separately; the existing roughly 15-minute block cadence can make this slow. 6. Cancellation releases buyer funds only after canonical cancellation/expiry is mature and all previously signed completion paths are invalid under consensus. Never refund on a website timer, mempool absence, RPC timeout or current lack of delivery alone. If completion and cancellation race, canonical rules choose one; an uncertain fork remains reserved/reviewed until resolved. NFT lock/completion/cancel rules are **not implemented or fully specified here**. Exact boundary heights, expiry, in-block ordering, seller authorization lifetime, host-key rotation/revocation, third-party submission of stored witnesses and fork behavior need a dedicated consensus specification and tests. Existing ordinary signed transfers cannot safely substitute because they can be submitted after a refund. If the host disappears, a reviewed height-expiry path should eventually return NFT control without a host signature. It cannot recover custodial XMR; financial recovery still depends on operator backups/keys. A stalled chain delays safe cancellation. This is a recoverable saga with explicit trust, not an atomic commit across chains. **Reorg after settlement:** pause affected withdrawals and resale, preserve the original postings, and enter review. If the same completion rematures, resolve without a second credit. If it is invalidated permanently, compensate with linked entries and prevent both a new delivery and buyer reimbursement. If proceeds have already left custody, use approved operator capital/loss procedures; never assume reversing a database entry recovers XMR or automatically confiscate unrelated balances. Confirmation/hold depths and funded exposure limits must reflect measured Wikideck chain security before launch. ## 7. Fees and small prices — proposed policy change Retain exact native-XMR listing prices, no economic listing minimum, current independent downward rounding and fixed 1% author allocation. Gross equals seller credit plus author credit plus host commission. Internal purchase settlement has **zero Monero network fee**; one-atomic-unit sales are possible when rounded commissions are zero. Resource/rate limits still apply. Propose: depositor pays their external sending fee; withdrawing beneficiary pays the quoted actual withdrawal fee; operator funds treasury sweeps/consolidation from disclosed operating capital. This replaces the earlier per-sale network-cost model **only if approved**. Do not run the existing per-sale payout-budget rejection unchanged for ledger purchases. Author fees accrue as separately identified, fully backed liabilities associated with the fixed policy recipient; do not assume a Monero address is a Wikideck login or let an admin silently redirect it. A reviewed address-control enrollment or fixed-destination payout procedure is required. They may accumulate until economical to pay, but cannot become host revenue; timing, fee payer and small/dormant balance treatment need explicit terms. This accumulation is proposed, not already authorized. No hidden withdrawal minimum or flat fee. Baseline: exact recipient amount plus a user-signed maximum fee; decline/defer before signing if it cannot be funded. A tiny balance remains owed even if a standalone withdrawal is currently uneconomic. Define fee-aware full-balance withdrawal, closure and unclaimed-funds handling before launch; do not promise every atomic balance can be withdrawn alone economically. Defer batching initially because it complicates fee allocation, privacy and partial outcomes. ## 8. Withdrawal protocol `requested → reserved → approved → preparing → prepared → relay-unknown/submitted → confirming → complete`, with `review` for uncertainty. Cancellation is straightforward only before creation of an externally usable signed transaction. - Fresh signed authorization binds custodian, both network identities, actor, destination, exact recipient amount, max fee, withdrawal ID/nonce, policy version and expiry. Verify Monero network/checksum; initially accept literal standard/subaddresses only, no DNS/OpenAlias or integrated-address ambiguity. Show full destination and total debit; use step-up authentication, new-destination cooling and independent approval above reviewed risk limits. - Atomically reserve amount plus max fee. A single fenced treasury worker allocates an attempt before wallet I/O. Check pooled unlocked liquidity, not the depositor's original outputs. Do not promise immediate withdrawals when change is locked or cold treasury replenishment is required. - Construct without relay, check actual fee/destination/amount/change against authorization, and durably encrypt/store signed transaction identity, metadata, selected-input identifiers and recovery material before broadcasting. `transfer` with `do_not_relay`/`get_tx_metadata` followed by `relay_tx` is an RPC capability, **not** a database transaction or automatic idempotency guarantee. Test the pinned wallet's construction/cache behavior, lost responses and input reservation explicitly. - On relay timeout/crash, reconcile or retransmit the **same** prepared transaction. Do not construct another payment or release the reservation just because an RPC says failed/not found. A signed transaction may still be relayed by someone holding it; replacement/cancellation requires a reviewed proof that the old spend can no longer succeed, including reorg exposure. Uncertain attempts stay quarantined. - After canonical withdrawal maturity, close liability once, account actual fee and release unused allowance. Preserve transaction keys/metadata securely for support; never expose secrets in logs. Outgoing reorgs reopen tracking, not a second withdrawal. No batch/automatic split in the first release; if construction yields multiple transactions, reject before relay or require an explicitly reviewed multi-transaction attempt model. ## 9. Custody operations, privacy and recovery - Recommend a view-only deposit observer plus an isolated, policy-constrained spending service and offline-signing/dual-control cold treasury. Cold storage and hot liquidity caps limit routine exposure; old deposit addresses may continue receiving into a hot wallet, so sweeping/rotation does not create a cryptographic total-deposit cap. A compromised signer can spend its wallet regardless of application limits. Pin and review the actual signing technology; do not assume arbitrary HSM/threshold compatibility with Monero. - Private fixed-origin authenticated RPC only; no public wallet RPC, browser credentials, arbitrary URL fetches or unrestricted proxy. Least privilege, method allowlists, independent signer policy, bounded bodies/timeouts, service authentication and encrypted transport where crossing hosts. Disable production capability by default and fail closed on network/profile mismatch. - Track daemon/wallet lag, canonical tip disagreement, reconciliation differences, queued/uncertain withdrawals, locked liquidity, deposit/reservation concentration and cold/hot exposure. Maintain separate switches for new deposits, new orders, author payouts and withdrawals. During incidents continue monitoring existing liabilities and provide a support/recovery path; blanket freezes are not a substitute for reconciliation. - Back up the financial journal/WAL, immutable customer-to-subaddress mapping and allocation high-water marks, wallet files/cache/seed under separate encrypted custody, policy versions, attestation keys, signed intents and prepared/outgoing transactions. Wallet seed alone cannot restore internal customer balances or all outgoing destination metadata. Restore into an isolated, non-spending environment; reconcile external transactions before enabling the single fenced signer. Drill full recovery and key compromise response. - Custodian learns deposit-to-Wikideck-wallet, purchase and withdrawal linkage despite Monero's public-chain privacy. Keep balances, addresses and history private/actor-scoped; minimize access/retention and redact analytics/logs. Audit exports and legal disclosure need controlled procedures. Hash-chained journal exports help tamper detection but do not prove honest custody or prevent privileged tampering without independent copies/review. - No automatic global wallet bans. Distinguish account-security restrictions, disputed cases and evidence-backed local service decisions; publish response, appeal and correction rules. - Obtain qualified US custody/money-transmission, sanctions, consumer, privacy and tax advice before funds are accepted. Decide customer asset segregation, insolvency treatment, loss allocation, closure and support obligations with counsel. No claim of insurance, legal exemption or guaranteed reimbursement without actual reviewed arrangements and funding. ## 10. Integration and staged acceptance Reuse [exact XMR amounts](../shared/xmr-amount.ts), [pricing splits](../shared/xmr-pricing.ts), [v3 mock domain](../shared/market-v3.ts) and receipt concepts in [market store](../server/market/store.ts). The [settlement lab](../server/market/settlement-lab.ts) provides failure-journal ideas only: its USD invoice and per-order external payout assumptions are not this balance model. Prefer direct pinned Monero wallet/daemon infrastructure for the first custody adapter; SHKeeper is not the customer-liability ledger and its documented USD invoicing is not needed for account deposits. No adapter is connected by this proposal. 1. **Approve scope and economics:** custodian identity, custody disclosure, account recovery, beneficiary enrollment, withdrawal/author fee policy, balance accumulation, loss/reserve obligations and numerical risk/confirmation/hold policies. These remain open; no fabricated thresholds or support SLA. 2. **Offline financial model:** property-test conservation, nonnegative available balances, concurrency, replay, output deduplication, tiny amounts, duplicate workers, crash at every journal/outbox boundary and repeat restoration. Keep mocks isolated; no mock balances become real opening liabilities. 3. **Separate NFT consensus specification:** lock/completion/cancel/expiry/host-attestation semantics, cross-market double sales, stale signatures, host disappearance and both fork races. Independent review, versioned network activation and migration approval; no silent changes to existing chain identity. 4. **Disposable Monero integration:** only with separate network/budget/access approval. Verify lossless RPC, output identity, wallet restore/lookahead, locked/double-output deposits, reorgs, construction-response loss, signer restrictions, relay ambiguity and withdrawal recovery. Independent security/accounting review and measured load/capacity tests. 5. **Operational release:** funded reserves and liquidity, counsel/terms, penetration test, audited restore/incident drills, customer statements/support and operator sign-off. Start with declared low exposure and manual withdrawal approvals; expand only after measured evidence. No live auction release. Required failure outcomes: duplicate deposit credits are impossible; insufficient/unlocked-pending funds cannot buy; two concurrent buys/withdrawals cannot overspend; delivery and refund cannot both mature; completed-sale replay cannot mint ledger credit; a lost relay response never causes a second payment; restored journals cannot resend old withdrawals; operator compensation never silently drains another customer; deep reorgs create visible cases rather than imaginary rollback of Monero. ## Primary sources and remaining evidence - [Monero subaddresses](https://docs.getmonero.org/public-address/subaddress/) and [wallet RPC `create_address`](https://docs.getmonero.org/rpc-library/wallet-rpc/#create_address): address allocation/indexing, not an internal financial ledger. - [Wallet-owned outputs](https://docs.getmonero.org/rpc-library/wallet-rpc/#incoming_transfers), [transfers](https://docs.getmonero.org/rpc-library/wallet-rpc/#get_transfers), [balances](https://docs.getmonero.org/rpc-library/wallet-rpc/#get_balance): output identity, subaddress association, synchronization, locking and balance observations. Exact production-version fields and stability must be fixture-tested. - [Constructing transactions](https://docs.getmonero.org/rpc-library/wallet-rpc/#transfer), [relay](https://docs.getmonero.org/rpc-library/wallet-rpc/#relay_tx), [offline signing](https://docs.getmonero.org/rpc-library/wallet-rpc/#sign_transfer): preparation/signing/relay separation; signed payloads must be checked, protected and recovered. - [Transfer history](https://docs.getmonero.org/rpc-library/wallet-rpc/#get_transfer_by_txid) and [rescan](https://docs.getmonero.org/rpc-library/wallet-rpc/#rescan_blockchain): wallet restore can lose outgoing destinations, transaction keys and notes. Blockchain replay is not a backup for customer accounting. Source review: 2026-10-20; documentation is capability evidence, not an audited implementation of this design. No wallet/RPC credentials or funds were accessed.