diff --git a/docs/financial-reconciliation.md b/docs/financial-reconciliation.md
new file mode 100755
index 0000000..f23f412
--- /dev/null
+++ b/docs/financial-reconciliation.md
@@ -0,0 +1,48 @@
+# Custodial business-liability reconciliation
+
+Status: implemented PostgreSQL **sandbox** integrity checks; not external proof of Monero backing or production settlement acceptance. Related: [production plan](production-plan.md), [financial storage](postgres-financial-storage.md). Updated 2026-10-20.
+
+## Why totals are insufficient
+
+A balanced journal and matching account projections can coexist with a corrupted order: its buyer, amount, status or pending task may disagree with the funds actually reserved. Aggregate assets equalling liabilities alone cannot detect a substituted recipient or orphan task.
+
+[Business integrity checks](../server/market/custody-integrity.ts) now validate both directions:
+
+- Strict persisted listing/order/withdrawal encodings, indexed identity/status columns and JSON record agreement.
+- Order card, seller, buyer, sequence, price and historical hosting recipient against the original listing; exact independently rounded seller/author/host allocation and fixed author policy. Current operator fee configuration never rewrites history.
+- Each awaiting-lock order has its exact credit-normal purchase reservation; cancelled/fixture-settled orders have zero reservation. The actual reservation and terminal journal lines must match the expected lifecycle, accounts and amounts, not just net to zero.
+- Each withdrawal hold equals recipient amount plus maximum approved fee. Prepared/complete fixture records require an attempt ID, unique transaction identity and fee within cap. Cancelled/complete holds are zero; terminal postings and returned unused fee allowance match exactly.
+- Blocked/cancelled/prepared/completed outbox state and payload correspond to the business record. Imported SQLite fixture terminal evidence is recognized explicitly; no real NFT/Monero settlement is inferred.
+- Reverse checks reject orphan reservation accounts, pending work and lifecycle postings, duplicate listing orders, and reused prepared attempt/transaction identities.
+
+Reads use the caller's coherent transaction. Record batches use UUID keyset pagination, maximum 128 per batch, with no growing process-wide customer cache. A 15-second business-scan budget is checked between batches/queries; the existing per-query timeout can extend the final boundary. This bounds application memory and failure behavior, **not a production capacity claim**. Very large histories may require indexed normalized schemas, incremental auditing and a dedicated database connection/worker budget before release.
+
+The existing exact journal/projection/backing audit remains, and now invokes these checks at startup, explicit audit, SQLite import validation and deployment checks. Targeted pre/post checks run inside order/withdrawal mutation transactions. No database schema version or old financial record is rewritten by this increment.
+
+## Runtime scheduling and failure behavior
+
+The PostgreSQL backend schedules one nonoverlapping audit every 60 seconds. Mutations are held if the latest audit failed or the last success is over 120 seconds old. A transient availability failure can recover after a successful later audit; it does not automatically classify customer behavior as fraud.
+
+A confirmed integrity mismatch latches the detecting process and stores a redacted `integrity_incident_v1` record in custody metadata. Subsequent writers check that marker under the existing cross-process advisory lock. Startup refuses it. Private activity and original signed receipt replay remain available; no new posting, refund, cancellation or withdrawal reservation is allowed while integrity is uncertain. This is deliberately stricter than an ordinary market-disabled flag, which still permits normal resolution.
+
+For a targeted write-path mismatch, a savepoint rolls back business work while retaining the writer lock; the incident is committed before another cooperating writer can proceed. A background/read-only audit or startup failure records its incident separately. **Process crash or database unavailability can prevent that marker from committing.** A redacted error explicitly requests fencing all writers if marker persistence fails. A periodic audit is not an atomic fraud-prevention barrier for every unrelated write, nor protection from a privileged operator or fully compromised application.
+
+Private loopback operations now expose only backend, mutation/incident hold, audit timestamps, freshness and a stable failure code. No customer ID, order ID, address, SQL statement, balance or connection secret is attached. [Operations checks](../server/operations-check.ts) emit `custody-financial-review` and `custody-audit-stale` warnings. No new public admin or financial audit endpoint was added.
+
+## Controlled resolution — never reset journals
+
+1. Stop/fence all custody writers and preserve matching database/node evidence and backups. Diagnose privately; a public-facing error intentionally does not disclose the affected customer.
+2. Have the responsible operator/reviewer establish the correct state from immutable receipts/journals, trusted backup evidence and any external effects. This increment supplies **no automatic repair, compensation, destructive reset or payout retry**. Repair/restore authorization is separate from acknowledging an incident.
+3. Using the schema-owner credential, run the existing database CLI action `resolve-integrity EVIDENCE_UUID`. The UUID refers to the externally maintained reviewed incident case; the tool does not certify the truth of that case or supply a second human approval.
+4. [Resolution code](../server/market/custody-incident.ts) takes the financial writer lock, verifies schema-owner membership, tentatively removes the unresolved marker and runs the full accounting/business audit in the same transaction. Failure rolls everything back, preserving the hold. Success appends a resolution record with evidence UUID, timestamp and incident digest; it changes no journal/account/business rows.
+5. Restart previously latched instances only after controlled resolution, then verify successful startup/audit and operations state. A detecting instance stays locally held even after a successful later audit. The restricted runtime role cannot acknowledge incidents or delete the marker.
+
+This process is intentionally not a “clear error” button. Manual SQL balance edits, deleting a marker outside the reviewed procedure, restoring stale isolated financial state or inventing journal resets can create duplicate payouts or loss of liabilities. An external database owner retains administrative power; independent evidence retention remains necessary.
+
+## Evidence and limitations
+
+Tests cover business-record corruptions that leave all journal totals balanced; bad allocations/fee metadata; missing/mismatched outbox; orphan reserves/tasks; targeted mutation rollback; durable cross-instance/startup holds; private reads/original replay during a hold; restricted-role resolution denial; failed-resolution rollback; successful reviewed acknowledgement plus restart; traversal beyond 128 records; and import of historical fixture completions/cancellations. No actual fund transfer, incident on deployed user data or consensus change was performed.
+
+Final validation: TypeScript/Vite Docker build passed; **168 offline regression tests and 16 real-PostgreSQL integration tests passed**, zero failures/skips. The first corruption-test run exposed an unguarded chained amount refinement on malformed stored decimals; the parser was corrected to emit a redacted integrity incident and all suites were rerun. Deployed sandbox services were not recreated or subjected to deliberate corruption; new code takes effect only after the selected runtime image is rebuilt/recreated.
+
+SQLite remains a disposable compatibility model with its original journal audit; this richer runtime checking is implemented in the PostgreSQL backend, including imported SQLite histories. Source-freeze/import procedures are unchanged. Production live-deposit attribution, treasury asset reconciliation, normalized financial states, compensation, observer/signing services, independent review and measured load/SLOs remain open. A successful internal audit is **not** proof of external XMR reserves, a guarantee of solvency, or launch permission.
diff --git a/docs/postgres-financial-storage.md b/docs/postgres-financial-storage.md
index a89880a..9043864 100755
--- a/docs/postgres-financial-storage.md
+++ b/docs/postgres-financial-storage.md
@@ -20,6 +20,8 @@ This is an integrated backend, not a second disconnected balance demo: [custody
- Pool maximum four connections per service; per-instance active admission sixteen; finite connect/statement/lock/idle transaction timeouts; synchronous commit explicitly on. Scale the total pool budget across instances deliberately. No wallet/network side effect is performed in a database transaction.
- No automatic retry after an uncertain commit. A client refreshes or resends the original signed envelope, letting durable receipts establish whether it committed. PostgreSQL failure never causes SQLite fallback. Startup audits compare every account projection with exact journal sums and check backing totals; these are internal accounting checks, not proof that real Monero assets exist.
+**Reconciliation hardening:** [financial business audits](financial-reconciliation.md) additionally check order/withdrawal encodings, exact allocations and held amounts, lifecycle journal lines, task payloads and orphan/duplicate records. They run at startup/import, on a nonoverlapping periodic schedule and on affected mutation boundaries. Persistent integrity incidents block writers and survive restart; only schema-owner acknowledgement with reviewed evidence and successful full audit can resolve them. External database/process loss still requires operational writer fencing. These checks do not prove real Monero backing.
+
These controls do not make a compromised application harmless: it still has posting privileges, and an administrative database owner can alter schema/history. Restricted roles prevent accidental/direct mutation, not fraudulent balanced postings by a fully compromised service. Independent monitoring, statement review, external backups and authorization/treasury isolation remain required.
## Configuration and operator workflow
diff --git a/docs/production-plan.md b/docs/production-plan.md
index 98286f7..ad87708 100755
--- a/docs/production-plan.md
+++ b/docs/production-plan.md
@@ -67,6 +67,8 @@ Can run alongside P0/P1. Addresses SEC-01/SEC-06 rather than adding more fixed c
**Next financial-storage increment:** [ADR 003 — PostgreSQL](postgres-financial-storage.md) selects and integrates the custody SQL backend with existing HTTP callers, exact constrained accounting, explicit migration/runtime roles, source-freezing SQLite import and dump/restore rehearsal. The adapter remains on the fictitious sandbox domain; it is not a production deposit/withdrawal worker. SQLite node-only backup deliberately refuses PostgreSQL-bound financial state. Production hardware/SLO tests, coordinated backup/PITR/failover, real payment schema/worker controls and independent review remain open, so this does not close M2/M4 or authorize funds.
+**Financial reconciliation increment:** [business-liability checks](financial-reconciliation.md) now tie each order/withdrawal to its exact reservation, journal lifecycle and pending work, including reverse orphan/duplicate checks. PostgreSQL startup/import/explicit/periodic audits and targeted mutations use these checks; redacted durable incident holds fence new postings, while private reads and original receipt replay remain available. Private operations reports audit freshness/holds, and schema-owner incident acknowledgement requires a successful locked full audit plus evidence reference. No automatic repair, real payment, schema reset or consensus activation is enabled. Production capacity, coordinated recovery and external review remain open.
+
**Deployment increment:** [persistent local custody stack](persistent-custody-sandbox.md) now starts PostgreSQL and the actual app together, with private TLS/SCRAM networking, separate owner/runtime roles, explicit migrations and scoped persistent credential/data volumes. A signed fictitious credit and custodian identity survived container/network removal and recreation; the server rejected plaintext authentication. Existing deployments and SQLite custody were not switched. Production certificate rotation, coordinated backup/restore and external operating review remain open; all XMR remains simulated.
- Indexed canonical ownership, NFT/transfer lookup and paged wallet/explorer APIs: browser signing must not require loading all history. Tip-pinned queries detect reorgs and preserve complete ownership semantics.
diff --git a/index.html b/index.html
index 1cd184d..fb7bb16 100644
--- a/index.html
+++ b/index.html
@@ -6,7 +6,7 @@
Wikideck — Mine knowledge.
-
+
diff --git a/public/original-logo.svg b/public/original-logo.svg
new file mode 100755
index 0000000..9ac970f
--- /dev/null
+++ b/public/original-logo.svg
@@ -0,0 +1 @@
+
diff --git a/scripts/custody-db.ts b/scripts/custody-db.ts
index b73612d..d179348 100755
--- a/scripts/custody-db.ts
+++ b/scripts/custody-db.ts
@@ -6,11 +6,12 @@ import { migrateCustody, grantCustodyRuntime } from '../server/market/postgres-s
import { importSqliteCustody } from '../server/market/custody-import.ts';
import { PostgresCustody } from '../server/market/postgres-custody.ts';
import { IS_MAINNET } from '../shared/network.ts';
+import { resolveCustodyIncident } from '../server/market/custody-incident.ts';
// Explicit operator task, never invoked by node startup. No passwords or database URLs printed.
if (IS_MAINNET) throw new Error('Only sandbox financial schema operations are implemented');
const [action, argument] = process.argv.slice(2);
-if (!['migrate', 'grant-runtime', 'import-sqlite', 'audit'].includes(action)) throw new Error('Expected migrate, grant-runtime ROLE, import-sqlite DATA_DIR, or audit');
+if (!['migrate', 'grant-runtime', 'import-sqlite', 'audit', 'resolve-integrity'].includes(action)) throw new Error('Expected migrate, grant-runtime ROLE, import-sqlite DATA_DIR, audit, or resolve-integrity EVIDENCE_UUID');
try {
const config = await postgresConfig();
if (action === 'audit') {
@@ -21,6 +22,7 @@ try {
try {
if (action === 'migrate') { await migrateCustody(pool); console.log('Custody PostgreSQL schema verified/migrated. No funds activated.'); }
if (action === 'grant-runtime') { await grantCustodyRuntime(pool, argument ?? ''); console.log('Runtime grants applied; no credentials printed.'); }
+ if (action === 'resolve-integrity') console.log(JSON.stringify(await resolveCustodyIncident(pool, argument ?? '')));
if (action === 'import-sqlite') {
if (!argument) throw new Error('Stopped source node DATA_DIR required');
const directory = resolve(argument);
diff --git a/server/market/custody-incident.ts b/server/market/custody-incident.ts
new file mode 100755
index 0000000..fc5dbe3
--- /dev/null
+++ b/server/market/custody-incident.ts
@@ -0,0 +1,29 @@
+import { randomUUID, createHash } from 'node:crypto';
+import type { Pool } from 'pg';
+import { z } from 'zod';
+import { CUSTODY_WRITE_LOCK } from './postgres-schema.ts';
+import { auditPostgres } from './postgres-custody.ts';
+
+/** Operator-only acknowledgement AFTER externally reviewed repair/restore; never repairs or resets a journal. */
+export async function resolveCustodyIncident(pool: Pool, evidenceId: string): Promise<{ resolutionId: string }> {
+ z.string().uuid().parse(evidenceId);
+ const c = await pool.connect();
+ try {
+ await c.query('BEGIN');
+ await c.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL idle_in_transaction_session_timeout='10s'; SET LOCAL synchronous_commit=on");
+ await c.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]);
+ const allowed = await c.query("SELECT pg_has_role(current_user,nspowner,'MEMBER') AS owns FROM pg_namespace WHERE nspname='wk_custody'");
+ if (allowed.rows[0]?.owns !== true) throw new Error('Schema-owner approval required for integrity resolution');
+ const incident = await c.query("SELECT value FROM wk_custody.custody_meta WHERE key='integrity_incident_v1' FOR UPDATE");
+ if (!incident.rowCount) throw new Error('No unresolved custody integrity incident');
+ // Delete is UNCOMMITTED and exclusively locked. Any audit failure rolls it back, leaving the hold intact.
+ await c.query("DELETE FROM wk_custody.custody_meta WHERE key='integrity_incident_v1'");
+ await auditPostgres(c);
+ const resolutionId = randomUUID();
+ await c.query('INSERT INTO wk_custody.custody_meta VALUES($1,$2)', [`integrity_resolution:${resolutionId}`,
+ JSON.stringify({ version: 1, evidenceId, resolvedAt: Date.now(),
+ incidentDigest: createHash('sha256').update(incident.rows[0].value).digest('hex') })]);
+ await c.query('COMMIT'); return { resolutionId };
+ } catch (error) { await c.query('ROLLBACK').catch(() => {}); throw error; }
+ finally { c.release(); }
+}
diff --git a/server/market/custody-integrity.ts b/server/market/custody-integrity.ts
new file mode 100755
index 0000000..a2a2c57
--- /dev/null
+++ b/server/market/custody-integrity.ts
@@ -0,0 +1,199 @@
+import { z } from 'zod';
+import type { PoolClient, QueryResult } from 'pg';
+import { ADDITIONAL_MARKET_FEE } from '../../shared/market.ts';
+import { parseAtomicXmr } from '../../shared/xmr-amount.ts';
+import { allocateXmrSale } from '../../shared/xmr-pricing.ts';
+import { validateXmrAddress } from './xmr.ts';
+
+/** Codes only: never attach customer IDs, payloads, SQL errors or addresses to operational diagnostics. */
+export class CustodyIntegrityError extends Error {
+ readonly code: string;
+ constructor(code: string) { super(`Custody integrity check failed: ${code}`); this.code = code; }
+}
+function fail(code: string): never { throw new CustodyIntegrityError(code); }
+const uuid = z.string().uuid(), hex = z.string().regex(/^[a-f0-9]{64}$/);
+const integer = z.number().int().nonnegative().safe();
+const atomic = z.string().max(22).refine(s => { try { parseAtomicXmr(s); return true; } catch { return false; } });
+const positive = atomic.refine(s => { try { return parseAtomicXmr(s) > 0n; } catch { return false; } });
+const wallet = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{95}$/);
+const listingSchema = z.object({ id: uuid, seller: hex, cardHash: hex, sequence: integer,
+ title: z.string().min(1).max(200), priceAtomicXmr: positive, feeBps: z.number().int().min(0).max(5000),
+ hostFeeAddress: wallet.optional(), status: z.enum(['open', 'reserved', 'cancelled', 'sold']), createdAt: integer }).strict();
+const orderSchema = z.object({ id: uuid, listingId: uuid, seller: hex, buyer: hex, cardHash: hex, sequence: integer,
+ grossAtomicXmr: positive, sellerAtomicXmr: atomic, authorAtomicXmr: atomic, hostAtomicXmr: atomic,
+ authorAddress: wallet, hostFeeAddress: wallet.optional(), status: z.enum(['awaiting-lock', 'cancelled', 'settled']), createdAt: integer }).strict();
+const withdrawalSchema = z.object({ id: uuid, actor: hex, destination: z.literal('mock-external'), amountAtomicXmr: positive,
+ maxFeeAtomicXmr: atomic, status: z.enum(['reserved', 'prepared', 'complete', 'cancelled']),
+ attemptId: uuid.optional(), transactionId: z.string().regex(/^fixture:[a-f0-9]{64}$/).optional(),
+ feeAtomicXmr: atomic.optional(), createdAt: integer }).strict();
+type Entry = { account: string; debit: string; credit: string };
+type Journal = { id: string; kind: string; lines: Entry[] };
+function parse(schema: z.ZodType, data: unknown, code: string): T {
+ const result = schema.safeParse(data); return result.success ? result.data : fail(code);
+}
+function same(a: unknown, b: unknown): boolean {
+ // Compare parsed records without depending on JSONB object key ordering.
+ if (a === b) return true;
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
+ const x = a as Record, y = b as Record;
+ const keys = Object.keys(x).filter(k => x[k] !== undefined).sort();
+ return keys.join('\0') === Object.keys(y).filter(k => y[k] !== undefined).sort().join('\0') && keys.every(k => same(x[k], y[k]));
+}
+function json(text: string | null): unknown { try { return JSON.parse(text ?? ''); } catch { return fail('outbox-encoding'); } }
+const d = (account: string, amount: string): Entry => ({ account, debit: amount, credit: '0' });
+const c = (account: string, amount: string): Entry => ({ account, debit: '0', credit: amount });
+async function journals(client: PoolClient, ids: string[]): Promise