invent): * - Classic three-band stage: Top ~65vh (search/wallet/book surface), Middle ~25vh (coach/stats), * Bottom ~10vh (unnumbered tabs Wallet / Send / Trade book). White 1px borders, black void. * - Slide-in animations (left/right/up). DNA line: TRADE · NSU rails · orange burn. * - Empire entrance (chat DNA): Options · Mirrors · Donate + I AGREE ENTER; wipe facts in modal/coach * NOT a permanent chrome wipe banner. Tabs never numbered 1/2/3. * * 0 PRIME: One file. No signup. Hard to kill. No course. No paywall on basics. * 1 FIRST RUN: Agree terms → get disposable wallet on entry → write 12 words * offline → confirm → claim free starter NSU. Stranger must get this without help. * 2 SEED: Seed is the only password. No recovery desk. Losing seed loses coins. * 3 NSU: Genesis+treasury = world-population-scaled optimum (not infinite mint). * MASTER FAUCET: genesis supply lives in the treasury wallet (pop × 1000 NSU/human). * That wallet IS the empire faucet for THIS site. Rewards = treasury transfers. * Renters must NOT receive treasury.secret / site wallet seed. * Optional donate path may grant NSU later; never a required purchase. * 4 RESET: Yearly wipe. ONLY user NSU wallet balances survive. Profit/ads/bids/ * orders/modifiers burn. Treasury re-anchors to (world_pop × optimum/human) * so liquidity restarts with the size of the world - predictable flux by * iteration+ephemerality; market has no structural reason to die. Wipe facts * (WARNING NEXT RESET IN ___ DAYS) live in entry modal + coach — no chrome banner. * Early window restarts. * RENT SIPHON (scaffold): at wipe, remaining renter-side profit/ad piles on * THIS crop feed the site treasury/master faucet path — renters never mint. * 5 BOOK: Signed intents; NSU leg on-site; foreign settle off-platform P2P. * Empty book after genesis/wipe → loose bootstrap spread (kind=bootstrap_spread) * around temp anchor 1.0 NOTE/NSU so discovery has a mid; makers tighten. * Market P = book converge; donate buyback arms at P/2 (never fixed sticker). * 6 NO THROAT: Non-custodial ghost for visitors. Site-local renter panel is * app-god-mode (incl. replace THIS file), not OS root. Independent wallets. * 7 SECURITY: Experimental until proven. Honest warnings > false guarantees. * Today: browser_hmac_v1 + ecdsa_p256_v1 (register pubkey once, then signed ops * without seed: transfer, order, cancel, fill, reputation, faucet claim). * Legacy seed-addr wallets keep working; v2 pubkey-hash addr still planned. * 8 MIRRORS: Swarm later; this node is disposable; source must stay auditable. * 9 PANEL: /controlpanel or ?controlpanel=1 — paste THIS site's wallet seed * (same as operator/treasury seed). No recovery desk. No password product path. * Unlock: seed derives treasury_addr (owner). Later rent may use economy.operator_addr. * 10 ECONOMY: site-local ads; donate→NSU via book mid×k then 50% bid / 50% profit. * * Genesis+treasury NSU · disposable seed wallets · transfers · order book * No downloads · no Monero/Haveno · no TradingView · no CDN · no DB daemon * rev 2026-07-13g - wave7: chain RMW lock transfer/order/admin_pay/extract/lazy_accrue TOCTOU */ /** * FILE MAP (cells/trade.php — physical order; comments only, not a second spec): * charter + PRODUCT CONTRACT ...... law / keep-working / never-become (above) * constants + paths ............... NST_* · $ROOT/$DATA/$CHAIN/$VAULT * identity / auth helpers ......... addr_from_seed · mac · ecdsa · nst_addr_v2 scaffold * brains + wipe copy .............. nst_brain_* · nst_ephemerality_reel_segments * chain / book / economy .......... locks · balances · bootstrap · faucet · ads * E2 conservation ............... nst_conservation_* · freeze file · write gates * panel / vault / admin ........... panel_seed_ok · require_admin · vault notes * routes .......................... ?api=* · ?src/?download · ?controlpanel=1 (early exit) * main HTML/CSS ................... Top/Mid/Bottom DNA · entry gate · unnumbered tabs * client JS IIFE .................. wallet/session · book · ads · coach · gate boot * Edit this cell only; sync-pack writes root/pack 4.php. Do not hand-edit pack. */ declare(strict_types=1); const NST_VERSION = '2026-07-13g'; /** * KING faucet liquid target / genesis = 24_000_000 NSU (operator 2026-07-15). * Micros = whole_NSU × 1e6. Fixed product supply (not live pop×1000). * Yearly wipe re-anchors liquid treasury to this target. User balances never seized. * Population crawl retained as metadata only. Protected PRODUCT CONTRACT text still * mentions world-scale wording; implementation law is 24M fixed. */ const NST_DECIMALS = 6; const NST_GENESIS_TREASURY = '24000000000000'; // 24_000_000 NSU const NST_MAX_SUPPLY = '24000000000000'; // soft display cap = same scale /** Legacy metadata constant only (old pop-scale docs); liquid target ignores this. */ const NST_OPTIMUM_MICROS_PER_HUMAN = '1000000000000'; /** Fail-open world population if crawl/cache unavailable (UN-scale estimate). */ const NST_POP_FALLBACK = 8200000000; /** Cache TTL for crawled population (seconds). */ const NST_POP_CACHE_SECS = 604800; // 7 days const NST_FAUCET_MICROS = '5000000'; const NST_ORDER_MAX = 200; const NST_DATA = 'data'; const NST_MOD_DAILY_MICROS = '100000'; // 0.1 NSU per day const NST_RESET_SECS = 31536000; // 365 days - yearly superstructure const NST_EARLY_WINDOW_SECS = 2592000; // 30 days after each yearly reset /** P3a rent quote: price = RENT_NSU_PER_DAY * days_left (whole NSU → micros). Payment settle = later residual. */ const NST_RENT_NSU_PER_DAY = 1000; const NST_RENT_QUOTE_TTL_SECS = 1800; // 30 minutes /** Dead address for treasury excess burn on re-anchor (not a user). */ const NST_BURN_ADDR = 'nsu-burn-v1-population-anchor'; /** When book mid missing: gift_nsu = external_amount * K (external in same unit as mid quote). */ const NST_DONATE_K = '1'; const NST_AD_MAX_PNG = 220000; // bytes - site-local weighted board const NST_AD_MAX_W = 2400; const NST_AD_MAX_H = 2400; const NST_AD_MIN_W = 16; const NST_AD_MIN_H = 16; /** Public static donate rails (tentative; vault holds private seeds). */ const NST_DONATE_BTC = 'bc1qqnu6n0jztxl4f6krv7klradghle09uhyu7uymz'; const NST_DONATE_XMR = '8Ab24DppUvcdtHfm7K8gTqdBTmPCBiak1GwxgPm1C3osYVQL2QdC1C8GMwggKF77RKKzDgP2R8E3VH8ifetsKms5AqkVyVg'; const NST_DONATE_LTC = 'ltc1qlpdy8qzejcmjdn6vwarpyz8djdlk780w4qkwyp'; /** Operator / recruit contact (not an account desk). */ const NST_CONTACT_EMAIL = 'buysellfreetrade@proton.me'; /** * LORD rent GRANT path — RETIRED 2026-07-16 (NSU_VIABILITY_V1 CUT #4 / atomic P2). * Rent is PAYMENT TO treasury only (pay-to-treasury + payer-key bind = P3, not this ship). * Constants below are DEAD; nst_lord_rent_credit_compute is dead code; admin_rent_claim returns 410. * Do not re-enable treasury→LORD credit or server-generated LORD seeds. * * KING faucet liquid scale = NST_KING_FAUCET_START_NSU (24_000_000). */ const LORD_RENT_CREDIT_LEGACY = 10000; // DEAD — retired flat placeholder (whole NSU) const NST_KING_FAUCET_START_NSU = 24000000; // whole NSU — genesis + re-anchor target /** @deprecated DEAD grant formula — do not use for product rent */ const LORD_RENT_PCT_BPS = 50; /** @deprecated DEAD grant formula */ const LORD_RENT_FLAT_BASE_NSU = 2500; /** @deprecated DEAD grant formula */ const LORD_RENT_FLAT_PERF_BPS = 100; /** @deprecated DEAD grant formula */ const LORD_RENT_FLAT_PERF_CAP_NSU = 50000; /** @deprecated DEAD grant formula */ const LORD_RENT_MIN_NSU = 1000; /** @deprecated DEAD grant formula */ const LORD_RENT_MAX_NSU = 500000; /** @deprecated DEAD — was API field compat for grant credit */ const LORD_RENT_CREDIT = LORD_RENT_CREDIT_LEGACY; /** When paid creatives empty (or luck roll): site-local house discovery ads. */ const NST_HOUSE_AD_CHANCE = 18; // percent chance to show house even if paid pool exists /** * Bootstrap discovery book (temp anchor only — market P still converges from live book). * Anchor 1.0 NOTE/NSU = 1_000_000 micros; loose BUY half / SELL 1.5x. * Size each side ≈ treasury / NST_BOOTSTRAP_DENOM (0.1% when denom=1000). */ const NST_BOOTSTRAP_ANCHOR_PX = '1000000'; // 1.0 NOTE per NSU (micros) const NST_BOOTSTRAP_BUY_PX = '500000'; // 0.5 const NST_BOOTSTRAP_SELL_PX = '1500000'; // 1.5 const NST_BOOTSTRAP_DENOM = 1000; // amount = floor(treasury/denom) per side /** * OPERATOR_GO §4 / E2a: KING real fillable SELL ladder (NOT bootstrap_spread). * Three rungs around discovery anchor; small fixed size so book has TAKE-able asks. * Each SELL arms C2 half-BID via nst_treasury_half_bid_from_sell. */ const NST_LADDER_AMT_NSU = '100'; // 100 NSU per rung (micros via parse_amt) const NST_LADDER_AMT_MAX_NSU = '1000'; // hard cap per rung (anti-drain) const NST_LADDER_PX_LO = '1000000'; // 1.0 NOTE/NSU const NST_LADDER_PX_MID = '1250000'; // 1.25 const NST_LADDER_PX_HI = '1500000'; // 1.5 /** E2: throttle full conservation replay (seconds). Freeze file always honored. */ const NST_CONSERVATION_RECHECK_SECS = 300; /** Wallet Inbox v0: short-lived sealed notices (mail≠mint). Aligned with bid ephemerality. */ const NST_INBOX_TTL_SECS = 604800; // 7 days default const NST_INBOX_MAX_CT_HEX = 8192; const NST_INBOX_ALG = 'ecdh_p256_hkdf_sha256_aes256gcm_v1'; $ROOT = __DIR__; $DATA = $ROOT . DIRECTORY_SEPARATOR . NST_DATA; $CHAIN = $DATA . DIRECTORY_SEPARATOR . 'chain.jsonl'; $TFILE = $DATA . DIRECTORY_SEPARATOR . 'treasury.secret'; // KING vault seed (only minter) $SITE_SEED_FILE = $DATA . DIRECTORY_SEPARATOR . 'site.seed'; // trade panel wallet seed (not mint) $META = $DATA . DIRECTORY_SEPARATOR . 'meta.json'; $ECON = $DATA . DIRECTORY_SEPARATOR . 'economy.json'; $ADS = $DATA . DIRECTORY_SEPARATOR . 'ads'; $ADMIN_HASH_FILE = $DATA . DIRECTORY_SEPARATOR . 'admin.pass.hash'; $ADMIN_PASS_FILE = $DATA . DIRECTORY_SEPARATOR . 'admin.pass.txt'; // legacy plaintext migrate $MODS_FILE = $DATA . DIRECTORY_SEPARATOR . 'modifiers.json'; $OP_ADDR_FILE = $DATA . DIRECTORY_SEPARATOR . 'operator.addr'; // site panel addr (≠ treasury/KING on fresh genesis) /** P3b: durable open rent quotes (JSON map). Not chain; settle residual reads this. */ $RENT_QUOTES_FILE = $DATA . DIRECTORY_SEPARATOR . 'rent_quotes.json'; $VAULT = dirname($ROOT) . DIRECTORY_SEPARATOR . 'vault'; // sibling of html/ function j($x): string { return json_encode($x, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } function norm_seed(string $s): string { return strtolower(trim(preg_replace('/\s+/', ' ', $s) ?? '')); } /** Path under durable $DATA — same path strings for L0/L1 money/identity (never RAM). */ function nst_data_file(string $name): string { global $DATA; return $DATA . DIRECTORY_SEPARATOR . $name; } /** * Ephemeral dir for regenerable L3 only (world_pop cache). * Prefer /dev/shm when present+writable; else durable $DATA/ephemeral/. * DENYLIST stays on disk: chain, treasury.secret, site.seed, rent_quotes, economy, * modifiers, operator.addr, admin.pass.hash, conservation.freeze, pubkey SoT. */ function nst_ephemeral_dir(): string { global $DATA; static $cached = null; if ($cached !== null) { return $cached; } if (is_dir('/dev/shm') && is_writable('/dev/shm')) { $d = '/dev/shm/nosignup_trade_ephemeral'; if ((is_dir($d) || @mkdir($d, 0700, true)) && is_writable($d)) { return $cached = $d; } } $d = $DATA . DIRECTORY_SEPARATOR . 'ephemeral'; if (!is_dir($d)) { @mkdir($d, 0755, true); } return $cached = $d; } /** First two hex chars of addr (256 fan-out). Fallback crc32%256 if short. */ function nst_addr_shard2(string $addr): string { $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; if (strlen($a) >= 2) { return substr($a, 0, 2); } return sprintf('%02x', abs(crc32($a)) % 256); } /** Legacy addr (v1): hash of seed. Future dual: ECDSA P-256 pubkey-hash addr + this legacy. */ function addr_from_seed(string $seed): string { return hash('sha256', 'nsu-addr-v1|' . norm_seed($seed)); } /** HMAC for browser_hmac_v1 (key includes seed → seed still needed to verify). */ function mac_sign(string $seed, string $msg): string { return hash_hmac('sha256', $msg, 'nsu-mac-v1|' . norm_seed($seed)); } /** ECDSA P-256 dual-path: seed-off-wire spend after pubkey registered for legacy addr. */ function nst_pubkey_path(string $addr): string { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; $aa = nst_addr_shard2($a); $dir = $DATA . DIRECTORY_SEPARATOR . 'keys' . DIRECTORY_SEPARATOR . $aa; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir . DIRECTORY_SEPARATOR . 'pubkey_' . $a . '.spki'; } /** Pre-footprint flat path (read fallback only; writes go to keys/{aa}/). */ function nst_pubkey_path_legacy(string $addr): string { global $DATA; return $DATA . DIRECTORY_SEPARATOR . 'pubkey_' . preg_replace('/[^a-f0-9]/', '', strtolower($addr)) . '.spki'; } function nst_addr_v2_from_spki_bin(string $spki): string { return hash('sha256', 'nsu-addr-v2|' . bin2hex($spki)); } /** IEEE P1363 (r||s, 64 bytes for P-256) → DER SEQUENCE for openssl_verify. */ function nst_ecdsa_p1363_to_der(string $p1363): ?string { if (strlen($p1363) !== 64) return null; $encInt = function (string $x): string { $x = ltrim($x, "\x00"); if ($x === '') $x = "\x00"; if ((ord($x[0]) & 0x80) !== 0) $x = "\x00" . $x; return "\x02" . chr(strlen($x)) . $x; }; $r = $encInt(substr($p1363, 0, 32)); $s = $encInt(substr($p1363, 32, 32)); $seq = $r . $s; return "\x30" . chr(strlen($seq)) . $seq; } /** True when host can verify ECDSA P-256 (OpenSSL extension). */ function nst_ecdsa_openssl_ready(): bool { return function_exists('openssl_verify') && function_exists('openssl_pkey_get_public'); } function nst_ecdsa_verify_p256(string $spkiBin, string $msg, string $sigP1363Hex): bool { if (!nst_ecdsa_openssl_ready()) return false; $sigRaw = @hex2bin(strtolower(preg_replace('/[^a-f0-9]/', '', $sigP1363Hex) ?? '')); if ($sigRaw === false || strlen($sigRaw) !== 64) return false; $der = nst_ecdsa_p1363_to_der($sigRaw); if ($der === null) return false; $pem = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($spkiBin), 64, "\n") . "-----END PUBLIC KEY-----\n"; $pub = @openssl_pkey_get_public($pem); if ($pub === false) return false; $ok = openssl_verify($msg, $der, $pub, OPENSSL_ALGO_SHA256); return $ok === 1; } function nst_load_registered_spki(string $addr): ?string { $p = nst_pubkey_path($addr); if (!is_file($p)) { $p = nst_pubkey_path_legacy($addr); } if (!is_file($p)) return null; $hex = trim((string)@file_get_contents($p)); $bin = @hex2bin(strtolower(preg_replace('/[^a-f0-9]/', '', $hex) ?? '')); return ($bin !== false && strlen($bin) > 40) ? $bin : null; } function nst_save_registered_spki(string $addr, string $spkiBin): bool { if (strlen($spkiBin) < 40) return false; return @file_put_contents(nst_pubkey_path($addr), bin2hex($spkiBin), LOCK_EX) !== false; } /** * WALLET INBOX v0 — address-keyed sealed mail (edit>invent on ECDSA/SPKI DNA). * KEY SEPARATION (documented): * - Signing key (ECDSA P-256): existing register_pubkey path (random key, AES-wrapped * at salt nsu-ecdsa-wrap-v1). Signs spends AND signs sealed inbox blobs. * - Encryption key (ECDH P-256): NEW, seed-derived (PBKDF2 salt nsu-ecdh-enc-v1, * 120000 iters → 32-byte scalar → PKCS8). Registered at encpub_.spki. * Distinct file + salt so sign≠encrypt. Recipient unlocks seed → re-derives priv → decrypts. * TRUE E2E: sender (KING panel client) encrypts; server stores OPAQUE ciphertext only. * Honesty: experimental until isolated e2e proof; never claim proven if plaintext touched server. * Mail ≠ mint: no NSU mint/balance change on seal/list/fetch/decrypt. */ function nst_encpub_path(string $addr): string { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; $aa = nst_addr_shard2($a); $dir = $DATA . DIRECTORY_SEPARATOR . 'keys' . DIRECTORY_SEPARATOR . $aa; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir . DIRECTORY_SEPARATOR . 'encpub_' . $a . '.spki'; } /** Pre-footprint flat path (read fallback only). */ function nst_encpub_path_legacy(string $addr): string { global $DATA; return $DATA . DIRECTORY_SEPARATOR . 'encpub_' . preg_replace('/[^a-f0-9]/', '', strtolower($addr)) . '.spki'; } function nst_load_registered_enc_spki(string $addr): ?string { $p = nst_encpub_path($addr); if (!is_file($p)) { $p = nst_encpub_path_legacy($addr); } if (!is_file($p)) return null; $hex = trim((string)@file_get_contents($p)); $bin = @hex2bin(strtolower(preg_replace('/[^a-f0-9]/', '', $hex) ?? '')); return ($bin !== false && strlen($bin) > 40) ? $bin : null; } function nst_save_registered_enc_spki(string $addr, string $spkiBin): bool { if (strlen($spkiBin) < 40) return false; return @file_put_contents(nst_encpub_path($addr), bin2hex($spkiBin), LOCK_EX) !== false; } /** * Wallet inbox blob path — sharded under data/inbox/{aa}/ (256 fan-out). * Filename keeps inbox_{addr}_{id}.blob so list/burn globs stay unambiguous. */ function nst_inbox_blob_path(string $addr, string $id): string { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; $i = preg_replace('/[^a-f0-9]/', '', strtolower($id)) ?? ''; $aa = nst_addr_shard2($a); $dir = $DATA . DIRECTORY_SEPARATOR . 'inbox' . DIRECTORY_SEPARATOR . $aa; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir . DIRECTORY_SEPARATOR . 'inbox_' . $a . '_' . $i . '.blob'; } /** Pre-footprint flat blob path (list/burn migration only). */ function nst_inbox_blob_path_legacy(string $addr, string $id): string { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; $i = preg_replace('/[^a-f0-9]/', '', strtolower($id)) ?? ''; return $DATA . DIRECTORY_SEPARATOR . 'inbox_' . $a . '_' . $i . '.blob'; } function nst_inbox_sign_body(array $b): string { return implode('|', [ 'inbox_seal', '1', (string)($b['to'] ?? ''), (string)($b['from'] ?? ''), (string)($b['id'] ?? ''), (string)($b['ts'] ?? ''), (string)($b['exp'] ?? ''), (string)($b['eph_pub'] ?? ''), (string)($b['iv'] ?? ''), (string)($b['ct'] ?? ''), ]); } /** Collect inbox blob paths for addr: sharded inbox/{aa}/ + legacy flat (migration). */ function nst_inbox_glob_for_addr(string $addr): array { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; if (strlen($a) !== 64) return []; $files = []; $aa = nst_addr_shard2($a); $shardDir = $DATA . DIRECTORY_SEPARATOR . 'inbox' . DIRECTORY_SEPARATOR . $aa; if (is_dir($shardDir)) { foreach (glob($shardDir . DIRECTORY_SEPARATOR . 'inbox_' . $a . '_*.blob') ?: [] as $f) { $files[] = $f; } } // Legacy flat data/inbox_{addr}_*.blob (pre-footprint) foreach (glob($DATA . DIRECTORY_SEPARATOR . 'inbox_' . $a . '_*.blob') ?: [] as $f) { $files[] = $f; } return array_values(array_unique($files)); } /** List non-expired sealed blobs for addr (metadata only; ct still opaque). */ function nst_inbox_list_for_addr(string $addr): array { $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; if (strlen($a) !== 64) return []; $out = []; $now = time(); foreach (nst_inbox_glob_for_addr($a) as $f) { $raw = (string)@file_get_contents($f); $j = json_decode($raw, true); if (!is_array($j)) continue; $exp = (int)($j['exp'] ?? 0); if ($exp > 0 && $exp < $now) { @unlink($f); continue; } $out[] = [ 'id' => (string)($j['id'] ?? ''), 'to' => (string)($j['to'] ?? ''), 'from' => (string)($j['from'] ?? ''), 'ts' => (int)($j['ts'] ?? 0), 'exp' => $exp, 'kind' => (string)($j['kind'] ?? 'king_note'), 'alg' => (string)($j['alg'] ?? ''), 'bytes' => strlen($raw), 'honesty' => (string)($j['honesty'] ?? 'experimental · sealed mail · not proven e2e'), ]; } usort($out, static fn($x, $y) => ($y['ts'] ?? 0) <=> ($x['ts'] ?? 0)); return $out; } /** * Yearly wipe / epoch burn: sealed notices die with bids (balances survive). * CRITICAL: must recurse inbox/{aa}/ shards — missed burn = stale blob regression. * Also burns legacy flat data/inbox_*.blob. */ function nst_inbox_burn_all(): int { global $DATA; $n = 0; // Legacy flat foreach (glob($DATA . DIRECTORY_SEPARATOR . 'inbox_*.blob') ?: [] as $f) { if (@unlink($f)) { $n++; } } // Sharded tree: data/inbox/{aa}/inbox_*.blob $root = $DATA . DIRECTORY_SEPARATOR . 'inbox'; if (is_dir($root)) { foreach (glob($root . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: [] as $dir) { foreach (glob($dir . DIRECTORY_SEPARATOR . 'inbox_*.blob') ?: [] as $f) { if (@unlink($f)) { $n++; } } } // Any blobs directly under inbox/ (defensive) foreach (glob($root . DIRECTORY_SEPARATOR . 'inbox_*.blob') ?: [] as $f) { if (@unlink($f)) { $n++; } } } return $n; } /** * INBOX auto-wire (money-adjacent): SERVER-COMPOSED notices of PUBLIC book events. * Honesty floor: this is NOT private E2E — the server sees the (public) content and * ECDH-encrypts for delivery only. KING hand-sealed client-encrypt notes remain the * only true-E2E path. Mail ≠ mint: never writes chain balances. * Money-safety: call ONLY after money ops commit; always fail-open (try/catch). */ function nst_inbox_ec_ready(): bool { return function_exists('openssl_pkey_new') && function_exists('openssl_pkey_derive') && function_exists('openssl_encrypt') && function_exists('hash_hkdf') && defined('OPENSSL_KEYTYPE_EC'); } /** Export P-256 public key as SPKI hex (uncompressed). Fail → ''. */ function nst_inbox_spki_hex_from_pkey($pkey): string { if ($pkey === false || $pkey === null) return ''; $det = @openssl_pkey_get_details($pkey); if (!is_array($det)) return ''; if (isset($det['ec']['x'], $det['ec']['y'])) { $x = $det['ec']['x']; $y = $det['ec']['y']; if (strlen($x) < 32) $x = str_pad($x, 32, "\0", STR_PAD_LEFT); if (strlen($y) < 32) $y = str_pad($y, 32, "\0", STR_PAD_LEFT); if (strlen($x) > 32) $x = substr($x, -32); if (strlen($y) > 32) $y = substr($y, -32); $hdr = @hex2bin('3059301306072a8648ce3d020106082a8648ce3d030107034200'); if ($hdr === false) return ''; return bin2hex($hdr . "\x04" . $x . $y); } $pem = (string)($det['key'] ?? ''); $b64 = preg_replace('/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s+/', '', $pem) ?? ''; $bin = base64_decode($b64, true); return ($bin !== false && strlen($bin) > 40) ? bin2hex($bin) : ''; } /** * Server-side ECDH→HKDF→AES-GCM seal (same wire as client inbox v0). * @return array{eph_pub:string,iv:string,ct:string}|null */ function nst_inbox_server_ecdh_seal(string $recipientSpkiBin, string $plaintext): ?array { if (!nst_inbox_ec_ready() || strlen($recipientSpkiBin) < 50) return null; try { $pem = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($recipientSpkiBin), 64, "\n") . "-----END PUBLIC KEY-----\n"; $their = @openssl_pkey_get_public($pem); if ($their === false) return null; $opts = ['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']; $cnf = getenv('OPENSSL_CONF'); if (is_string($cnf) && $cnf !== '' && is_file($cnf)) { $opts['config'] = $cnf; } $eph = @openssl_pkey_new($opts); if ($eph === false) return null; $shared = @openssl_pkey_derive($their, $eph); if ($shared === false || $shared === '') return null; $key = hash_hkdf('sha256', $shared, 32, 'nsu-inbox-v1', ''); if (!is_string($key) || strlen($key) !== 32) return null; $iv = random_bytes(12); $tag = ''; $ct = @openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16); if ($ct === false || strlen($tag) !== 16) return null; $ephHex = nst_inbox_spki_hex_from_pkey($eph); if (strlen($ephHex) < 100) return null; return [ 'eph_pub' => strtolower($ephHex), 'iv' => bin2hex($iv), 'ct' => bin2hex($ct . $tag), ]; } catch (Throwable $e) { return null; } } /** * Best-effort deliver a server_notice to $toAddr if enc-pubkey registered. * Fail-open: any error → skipped; never throws; never touches balances. * @param array $publicFacts public book facts only (no seeds/keys) * @return array{ok:bool,skipped?:bool,reason?:string,id?:string} */ function nst_inbox_deliver_server_notice(string $toAddr, string $event, array $publicFacts): array { try { $to = preg_replace('/[^a-f0-9]/', '', strtolower($toAddr)) ?? ''; if (strlen($to) !== 64) { return ['ok' => false, 'skipped' => true, 'reason' => 'bad_addr']; } $spki = nst_load_registered_enc_spki($to); if ($spki === null) { return ['ok' => false, 'skipped' => true, 'reason' => 'no_enc_pubkey']; } // Strip anything that must never appear in a notice (defense in depth). $safeFacts = []; foreach ($publicFacts as $k => $v) { $kk = preg_replace('/[^a-z0-9_]/', '', strtolower((string)$k)) ?? ''; if ($kk === '' || in_array($kk, ['seed', 'secret', 'treasury_secret', 'priv', 'private_key', 'pkcs8'], true)) { continue; } if (is_scalar($v) || $v === null) { $safeFacts[$kk] = $v; } else { $safeFacts[$kk] = substr(j($v), 0, 400); } } $payload = [ 'kind' => 'server_notice', 'event' => $event, 'facts' => $safeFacts, 'note' => 'server-composed notice of a public book event — encrypted for delivery, not a private secret; the event is public on the book', 'e2e' => false, 'server_composed' => true, 'mail_not_mint' => true, ]; $plaintext = j($payload); if (strlen($plaintext) > 1800) { $plaintext = substr($plaintext, 0, 1800); } $sealed = nst_inbox_server_ecdh_seal($spki, $plaintext); if ($sealed === null) { return ['ok' => false, 'skipped' => true, 'reason' => 'crypto_unavailable']; } if (strlen($sealed['ct']) > NST_INBOX_MAX_CT_HEX) { return ['ok' => false, 'skipped' => true, 'reason' => 'ct_too_large']; } $ts = time(); $ttl = NST_INBOX_TTL_SECS; $exp = $ts + $ttl; $factId = (string)($safeFacts['id'] ?? $safeFacts['order_id'] ?? $safeFacts['quote_id'] ?? ''); $id = substr(hash('sha256', 'server_notice|v1|' . $event . '|' . $to . '|' . $factId . '|' . (string)$ts . '|' . bin2hex(random_bytes(4))), 0, 32); $from = ''; try { $from = strtolower(addr_from_seed(treasury_secret())); } catch (Throwable $e) { $from = str_repeat('0', 64); } $blob = [ 'v' => 1, 'id' => $id, 'to' => $to, 'from' => $from, 'ts' => $ts, 'ttl' => $ttl, 'exp' => $exp, 'eph_pub' => $sealed['eph_pub'], 'iv' => $sealed['iv'], 'ct' => $sealed['ct'], 'alg' => NST_INBOX_ALG, 'kind' => 'server_notice', 'event' => $event, 'mail_not_mint' => true, 'server_composed' => true, 'honesty' => 'server-composed notice of a public book event — encrypted for delivery, not a private secret; the event is public on the book', 'sig_mode' => 'server_mac', ]; try { $sec = treasury_secret(); $blob['sig'] = mac_sign($sec, nst_inbox_sign_body($blob)); } catch (Throwable $e) { $blob['sig'] = ''; } $path = nst_inbox_blob_path($to, $id); if (@file_put_contents($path, j($blob), LOCK_EX) === false) { return ['ok' => false, 'skipped' => true, 'reason' => 'disk']; } return ['ok' => true, 'id' => $id, 'to' => $to, 'kind' => 'server_notice', 'event' => $event]; } catch (Throwable $e) { return ['ok' => false, 'skipped' => true, 'reason' => 'exception']; } } /** After fill commits: notify maker + taker (public fill facts). Fail-open. */ function nst_inbox_notify_fill(array $fill): void { try { $facts = [ 'event' => 'fill', 'order_id' => (string)($fill['order_id'] ?? ''), 'pair' => (string)($fill['pair'] ?? ''), 'side' => (string)($fill['side'] ?? ''), 'amount' => (string)($fill['amount_fmt'] ?? $fill['amount'] ?? ''), 'price' => (string)($fill['price_fmt'] ?? $fill['price'] ?? ''), 'maker' => (string)($fill['maker'] ?? ''), 'taker' => (string)($fill['taker'] ?? ''), 'ts' => (int)($fill['ts'] ?? time()), 'id' => (string)($fill['order_id'] ?? '') . '|' . (string)($fill['ts'] ?? '') . '|' . (string)($fill['taker'] ?? ''), ]; $maker = preg_replace('/[^a-f0-9]/', '', strtolower((string)($fill['maker'] ?? ''))) ?? ''; $taker = preg_replace('/[^a-f0-9]/', '', strtolower((string)($fill['taker'] ?? ''))) ?? ''; if (strlen($maker) === 64) { nst_inbox_deliver_server_notice($maker, 'fill', $facts); } if (strlen($taker) === 64 && $taker !== $maker) { nst_inbox_deliver_server_notice($taker, 'fill', $facts); } } catch (Throwable $e) { // fail-open } } /** After half-BID arms: notify treasury-sell context (public book floor). Fail-open. */ function nst_inbox_notify_half_bid(?array $sellOrder, array $halfBid): void { try { if (!empty($halfBid['skipped'])) { return; } $facts = [ 'event' => 'half_bid_armed', 'half_bid_id' => (string)($halfBid['id'] ?? ''), 'paired_sell_id' => (string)($halfBid['paired_sell_id'] ?? ($sellOrder['id'] ?? '')), 'pair' => (string)($halfBid['pair'] ?? ($sellOrder['pair'] ?? '')), 'amount' => (string)($halfBid['amount_fmt'] ?? $halfBid['amount'] ?? ''), 'price' => (string)($halfBid['price_fmt'] ?? $halfBid['price'] ?? ''), 'side' => 'buy', 'order_kind' => 'treasury_half_bid', 'ts' => (int)($halfBid['ts'] ?? time()), 'id' => (string)($halfBid['id'] ?? ''), ]; $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($sellOrder['addr'] ?? $halfBid['addr'] ?? ''))) ?? ''; if (strlen($to) === 64) { nst_inbox_deliver_server_notice($to, 'half_bid_armed', $facts); } } catch (Throwable $e) { // fail-open } } /** After rent settle commits: notify payer. Fail-open. */ function nst_inbox_notify_rent_settle(array $settle): void { try { if (empty($settle['settled'])) { return; } $payer = preg_replace('/[^a-f0-9]/', '', strtolower((string)($settle['payer'] ?? ''))) ?? ''; $lease = is_array($settle['lease'] ?? null) ? $settle['lease'] : []; $facts = [ 'event' => 'rent_settle', 'site' => (string)($settle['site'] ?? $lease['site'] ?? ''), 'quote_id' => (string)($settle['quote_id'] ?? $lease['quote_id'] ?? ''), 'payer' => $payer, 'amount' => (string)($lease['amount_fmt'] ?? $lease['amount'] ?? ''), 'ts' => (int)($lease['rented_at'] ?? time()), 'id' => (string)($settle['quote_id'] ?? ''), ]; if (strlen($payer) === 64) { nst_inbox_deliver_server_notice($payer, 'rent_settle', $facts); } } catch (Throwable $e) { // fail-open } } /** After rent refund commits: notify payer. Fail-open. */ function nst_inbox_notify_rent_refund(?array $refund): void { try { if (!is_array($refund) || empty($refund['refunded'])) { return; } $tx = is_array($refund['tx'] ?? null) ? $refund['tx'] : []; $payer = preg_replace('/[^a-f0-9]/', '', strtolower((string)($tx['to'] ?? ''))) ?? ''; $facts = [ 'event' => 'rent_refund', 'quote_id' => (string)($refund['quote_id'] ?? $tx['quote_id'] ?? ''), 'reason' => (string)($refund['reason'] ?? $tx['rent_refund_reason'] ?? ''), 'amount' => (string)($tx['amount_fmt'] ?? $tx['amount'] ?? ''), 'payer' => $payer, 'ts' => (int)($tx['ts'] ?? time()), 'id' => (string)($tx['rent_refund_of_nonce'] ?? $tx['nonce'] ?? ''), ]; if (strlen($payer) === 64) { nst_inbox_deliver_server_notice($payer, 'rent_refund', $facts); } } catch (Throwable $e) { // fail-open } } /** * Dual-path actor: ECDSA seed-off-wire candidate OR require_seed. * Registry state decides: if acting addr has a stored SPKI, seed/HMAC is refused * (ECDSA-only). ECDSA candidate when seed empty and pubkey+ecdsa_sig present → * addr from POST[$addrKey] (falls back to POST[from] then POST[addr]). * Signature verified later via nst_auth_sig. * * @return array{addr:string,seed:?string,ecdsa:bool,spki_hex:string,ecdsa_sig:string} */ function nst_actor(string $addrKey = 'from'): array { $seedRaw = trim((string)($_POST['seed'] ?? '')); $spkiHex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['pubkey'] ?? '')) ?? ''); $ecdsaSig = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['ecdsa_sig'] ?? '')) ?? ''); if ($seedRaw === '' && $spkiHex !== '' && $ecdsaSig !== '') { $addr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST[$addrKey] ?? ''))) ?? ''; if (strlen($addr) !== 64) { $addr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['from'] ?? ''))) ?? ''; } if (strlen($addr) !== 64) { $addr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['addr'] ?? ''))) ?? ''; } if (strlen($addr) !== 64) { api_out(['ok' => false, 'err' => 'bad addr'], 400); } return [ 'addr' => $addr, 'seed' => null, 'ecdsa' => true, 'spki_hex' => $spkiHex, 'ecdsa_sig' => $ecdsaSig, ]; } [$seed, $addr] = require_seed(); // ATOM1: registered SPKI ⇒ refuse seed/HMAC spend (path by registry, not seed emptiness) if (nst_load_registered_spki($addr) !== null) { api_out(['ok' => false, 'err' => 'address is ECDSA-registered; sign with your key, do not post seed'], 403); } return [ 'addr' => $addr, 'seed' => $seed, 'ecdsa' => false, 'spki_hex' => '', 'ecdsa_sig' => '', ]; } /** * Verify signature over $body for an nst_actor() result. * ECDSA: registered SPKI + P-256 P1363 sig (override via $ecdsaSigOverride for secondary legs). * HMAC: optional client_sig ($clientSigKey) → browser_hmac_v1; else server_mac. * * @return array{sig_mode:string,sig:string,pubkey:string,seed_on_wire:bool} */ function nst_auth_sig(array $actor, string $body, string $clientSigKey = 'client_sig', ?string $ecdsaSigOverride = null): array { if (!empty($actor['ecdsa'])) { $spkiHex = (string)($actor['spki_hex'] ?? ''); $ecdsaSig = $ecdsaSigOverride !== null ? strtolower(preg_replace('/[^a-f0-9]/', '', $ecdsaSigOverride) ?? '') : (string)($actor['ecdsa_sig'] ?? ''); $spki = @hex2bin($spkiHex); if ($spki === false || strlen($spki) < 50) { api_out(['ok' => false, 'err' => 'bad pubkey'], 400); } $reg = nst_load_registered_spki((string)$actor['addr']); if ($reg === null || !hash_equals($reg, $spki)) { api_out(['ok' => false, 'err' => 'pubkey not registered — unlock with seed once'], 403); } if ($ecdsaSig === '' || !nst_ecdsa_verify_p256($spki, $body, $ecdsaSig)) { api_out(['ok' => false, 'err' => 'bad ecdsa signature'], 400); } return [ 'sig_mode' => 'ecdsa_p256_v1', 'sig' => $ecdsaSig, 'pubkey' => $spkiHex, 'seed_on_wire' => false, ]; } $seed = (string)($actor['seed'] ?? ''); if ($seed === '') { api_out(['ok' => false, 'err' => 'Need your 12-word seed'], 400); } $expect = mac_sign($seed, $body); $clientSig = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST[$clientSigKey] ?? ''))) ?? ''; $sigMode = 'server_mac'; if ($clientSig !== '') { if (strlen($clientSig) !== 64 || !hash_equals($expect, $clientSig)) { api_out(['ok' => false, 'err' => 'bad client signature — re-unlock and retry'], 400); } $sigMode = 'browser_hmac_v1'; } return [ 'sig_mode' => $sigMode, 'sig' => $expect, 'pubkey' => '', 'seed_on_wire' => true, ]; } function dec_ok(string $a): bool { return (bool)preg_match('/^[0-9]+$/', $a); } function dec_norm(string $a): string { $a = ltrim($a, '0'); return $a === '' ? '0' : $a; } function dec_cmp(string $a, string $b): int { $a = dec_norm($a); $b = dec_norm($b); if (strlen($a) !== strlen($b)) return strlen($a) < strlen($b) ? -1 : 1; return $a <=> $b; } function dec_add(string $a, string $b): string { $a = dec_norm($a); $b = dec_norm($b); if (function_exists('bcadd')) return bcadd($a, $b, 0); $i = strlen($a) - 1; $j = strlen($b) - 1; $c = 0; $r = ''; while ($i >= 0 || $j >= 0 || $c) { $s = $c + ($i >= 0 ? (int)$a[$i--] : 0) + ($j >= 0 ? (int)$b[$j--] : 0); $r = (string)($s % 10) . $r; $c = intdiv($s, 10); } return dec_norm($r); } function dec_sub(string $a, string $b): string { if (dec_cmp($a, $b) < 0) return '-1'; if (function_exists('bcsub')) return bcsub(dec_norm($a), dec_norm($b), 0); $a = str_pad(dec_norm($a), max(strlen($a), strlen($b)), '0', STR_PAD_LEFT); $b = str_pad(dec_norm($b), strlen($a), '0', STR_PAD_LEFT); $c = 0; $r = ''; for ($i = strlen($a) - 1; $i >= 0; $i--) { $d = (int)$a[$i] - $c - (int)$b[$i]; if ($d < 0) { $d += 10; $c = 1; } else { $c = 0; } $r = (string)$d . $r; } return dec_norm($r); } function dec_mul_small(string $a, int $m): string { if (function_exists('bcmul')) return bcmul(dec_norm($a), (string)$m, 0); $a = dec_norm($a); $c = 0; $r = ''; for ($i = strlen($a) - 1; $i >= 0; $i--) { $p = (int)$a[$i] * $m + $c; $r = (string)($p % 10) . $r; $c = intdiv($p, 10); } while ($c > 0) { $r = (string)($c % 10) . $r; $c = intdiv($c, 10); } return dec_norm($r); } function fmt_amt(string $micros): string { $micros = dec_norm($micros); if (strlen($micros) <= NST_DECIMALS) { $whole = '0'; $frac = str_pad($micros, NST_DECIMALS, '0', STR_PAD_LEFT); } else { $whole = substr($micros, 0, -NST_DECIMALS); $frac = substr($micros, -NST_DECIMALS); } $frac = rtrim($frac, '0'); return $frac === '' ? $whole : "$whole.$frac"; } function parse_amt(string $h): ?string { $h = trim($h); if (!preg_match('/^\d+(\.\d{1,' . NST_DECIMALS . '})?$/', $h)) return null; if (strpos($h, '.') === false) return dec_mul_small($h, (int)str_pad('1', NST_DECIMALS + 1, '0')); [$w, $f] = explode('.', $h, 2); $f = str_pad($f, NST_DECIMALS, '0', STR_PAD_RIGHT); return dec_add(dec_mul_small($w === '' ? '0' : $w, (int)str_pad('1', NST_DECIMALS + 1, '0')), dec_norm($f)); } /* ---- TINY PURPOSE BRAINS (hand-crafted, in-file, no external deps) ---- * Each brain does ONE job. Sentience = many small coherent specialists, * not one cloud oracle. Fail open when unsure. */ /** Brain: memo_risk - bag-of-words scam/phishing score in [0,1]. Fail open. */ function nst_brain_memo_risk(string $text): array { $t = strtolower($text); $hits = []; $bad = [ 'seed phrase' => 0.35, 'private key' => 0.4, 'send all' => 0.25, 'double your' => 0.3, 'airdrop claim' => 0.25, 'connect wallet' => 0.2, 'verify wallet' => 0.25, 'urgent' => 0.1, 'guaranteed' => 0.15, '100x' => 0.2, 'giveaway' => 0.1, ]; $score = 0.0; foreach ($bad as $k => $w) { if ($k !== '' && str_contains($t, $k)) { $score += $w; $hits[] = $k; } } if ($score > 1.0) $score = 1.0; return ['brain' => 'memo_risk', 'score' => round($score, 3), 'hits' => $hits, 'flag' => $score >= 0.45]; } /** Brain: order_sanity - soft checks on side/amount/price; never blocks alone. */ function nst_brain_order_sanity(string $side, string $amount, string $price): array { $notes = []; $score = 0.0; if (!in_array($side, ['buy', 'sell'], true)) { $notes[] = 'side'; $score += 0.5; } if (dec_cmp($amount, '0') <= 0) { $notes[] = 'amount'; $score += 0.4; } if (dec_cmp($price, '0') <= 0) { $notes[] = 'price'; $score += 0.4; } // huge size relative to max supply micros is absurd if (strlen(dec_norm($amount)) > 18) { $notes[] = 'amount_huge'; $score += 0.3; } if ($score > 1.0) $score = 1.0; return ['brain' => 'order_sanity', 'score' => round($score, 3), 'notes' => $notes, 'flag' => $score >= 0.5]; } /** * Brain: wipe_copy + ephemerality reel - explicit yearly law for news-reel banner. * Segments are short so the reel can use larger type on Android (screen efficiency). * Coherence: same facts as gate risks / YEAR_WIPE vault note / EMPIRE_ECONOMICS. * * @return list */ function nst_ephemerality_reel_segments(int $daysLeft, ?array $extra = null): array { $d = max(0, $daysLeft); $segs = [ 'WARNING | NEXT YEARLY WIPE IN ' . $d . ' DAYS', 'EPHEMERALITY | this crop forgets on purpose every year', 'SURVIVES | ONLY your NSU wallet balances (ledger)', 'BURNS | profit pile | ad stakes | creatives | open orders | mods | panel loot', 'RE-KEYS | panel password dies at year end (leaks only hurt until wipe)', 'TREASURY | re-anchors to 24_000_000 NSU KING faucet (liquid gas, not your coins)', 'VALUE | THIS server owner sets local k / value note | not mirrors | not a global bank', 'MIRRORS | free tributaries only | help the network | zero cut | no value authority | no daily faucet wage until hits are parent-verifiable | host because the swarm is hard to kill', 'EARLY DRIP | permanent 0.1 NSU/day only if you unlock in first 30 days after reset', 'SEED | your 12 words are the only password | no desk | no refunds', ]; if (is_array($extra)) { foreach ($extra as $s) { $s = trim((string)$s); if ($s !== '') $segs[] = $s; } } return $segs; } /** Single-line join (legacy callers / static SSR). */ function nst_brain_wipe_copy(int $daysLeft): string { return implode(' | ', nst_ephemerality_reel_segments($daysLeft)); } /** * Brain: ad_png - PNG magic + IHDR geometry + byte budget. * Hard reject: corrupt / wrong magic / out-of-range dimensions / oversize bytes. * Soft flag: extreme aspect or huge pixel count (still may serve). Fail open on soft only. */ function nst_brain_ad_png(string $bin): array { $len = strlen($bin); if ($len < 33) { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'png too small', 'score' => 1.0, 'notes' => ['tiny'], 'flag' => true]; } if ($len > NST_AD_MAX_PNG) { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'png too big (max ' . NST_AD_MAX_PNG . 'b)', 'score' => 1.0, 'notes' => ['bytes'], 'flag' => true, 'bytes' => $len]; } if (substr($bin, 0, 8) !== "\x89PNG\r\n\x1a\n") { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'not a PNG', 'score' => 1.0, 'notes' => ['magic'], 'flag' => true]; } if (substr($bin, 12, 4) !== 'IHDR') { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'missing IHDR', 'score' => 1.0, 'notes' => ['ihdr'], 'flag' => true]; } $w = unpack('N', substr($bin, 16, 4))[1]; $h = unpack('N', substr($bin, 20, 4))[1]; $bit = ord($bin[24]); $color = ord($bin[25]); if ($w < NST_AD_MIN_W || $h < NST_AD_MIN_H) { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'min ' . NST_AD_MIN_W . 'x' . NST_AD_MIN_H, 'w' => $w, 'h' => $h, 'score' => 1.0, 'notes' => ['min'], 'flag' => true]; } if ($w > NST_AD_MAX_W || $h > NST_AD_MAX_H) { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'max ' . NST_AD_MAX_W . 'x' . NST_AD_MAX_H, 'w' => $w, 'h' => $h, 'score' => 1.0, 'notes' => ['max'], 'flag' => true]; } $notes = []; $score = 0.0; $ratio = $w / max(1, $h); if ($ratio > 12.0 || $ratio < (1.0 / 12.0)) { $notes[] = 'extreme_aspect'; $score += 0.35; } if (($w * $h) > 2000000) { $notes[] = 'megapixels'; $score += 0.25; } if (!in_array($color, [0, 2, 3, 4, 6], true)) { $notes[] = 'color_type'; $score += 0.5; } if ($bit !== 8 && $bit !== 16 && $bit !== 4 && $bit !== 2 && $bit !== 1) { $notes[] = 'bit_depth'; $score += 0.2; } // Prefer board-friendly widths (soft) if ($w < 120 && $h < 60) { $notes[] = 'tiny_display'; $score += 0.15; } if ($score > 1.0) { $score = 1.0; } return [ 'brain' => 'ad_png', 'ok' => true, 'score' => round($score, 3), 'notes' => $notes, 'flag' => $score >= 0.5, 'w' => $w, 'h' => $h, 'bit_depth' => $bit, 'color_type' => $color, 'bytes' => $len, 'ratio' => round($ratio, 3), ]; } /** Brain: href_risk - click-through URL scheme + soft phishing tokens. Hard reject non-http(s). */ function nst_brain_href_risk(string $href): array { $href = trim($href); if ($href === '') { return ['brain' => 'href_risk', 'ok' => true, 'score' => 0.0, 'hits' => [], 'flag' => false]; } if (preg_match('#^(javascript|data|file|vbscript):#i', $href)) { return ['brain' => 'href_risk', 'ok' => false, 'err' => 'blocked scheme', 'score' => 1.0, 'hits' => ['scheme'], 'flag' => true]; } if (!preg_match('#^https?://#i', $href)) { return ['brain' => 'href_risk', 'ok' => false, 'err' => 'href must be http(s)', 'score' => 1.0, 'hits' => ['scheme'], 'flag' => true]; } $t = strtolower($href); $hits = []; $bad = [ 'seed' => 0.2, 'privatekey' => 0.3, 'wallet-connect' => 0.15, 'airdrop' => 0.15, 'double-your' => 0.25, 'free-money' => 0.2, 'bit.ly' => 0.1, 'tinyurl' => 0.1, ]; $score = 0.0; foreach ($bad as $k => $w) { if (str_contains($t, $k)) { $score += $w; $hits[] = $k; } } if ($score > 1.0) { $score = 1.0; } return ['brain' => 'href_risk', 'ok' => true, 'score' => round($score, 3), 'hits' => $hits, 'flag' => $score >= 0.45]; } /** Brain: pair_sane - soft book pair text (NSU/NOTE etc); never blocks alone. */ function nst_brain_pair_sane(string $pair): array { $p = strtoupper(trim($pair)); $notes = []; $score = 0.0; if ($p === '' || !preg_match('#^[A-Z0-9]{2,12}/[A-Z0-9]{2,12}$#', $p)) { $notes[] = 'format'; $score += 0.4; } if (str_contains($p, 'SEED') || str_contains($p, 'HTTP')) { $notes[] = 'token'; $score += 0.4; } if ($score > 1.0) { $score = 1.0; } return ['brain' => 'pair_sane', 'score' => round($score, 3), 'notes' => $notes, 'flag' => $score >= 0.5, 'pair' => $p]; } function boot_data(): void { global $DATA, $CHAIN, $TFILE, $META; if (!is_dir($DATA)) @mkdir($DATA, 0755, true); $ht = $DATA . '/.htaccess'; if (!is_file($ht)) @file_put_contents($ht, "Require all denied\nDeny from all\n"); $idx = $DATA . '/index.html'; if (!is_file($idx)) @file_put_contents($idx, ''); if (!is_file($CHAIN)) @file_put_contents($CHAIN, ''); } function read_chain(): array { global $CHAIN; if (!is_file($CHAIN)) return []; $out = []; $fh = fopen($CHAIN, 'rb'); if (!$fh) return []; while (($line = fgets($fh)) !== false) { $line = trim($line); if ($line === '') continue; $j = json_decode($line, true); if (is_array($j)) $out[] = $j; } fclose($fh); return $out; } function append_row(array $row): bool { global $CHAIN; // E2 FREEZE: block every chain append while conservation freeze is sticky. if (function_exists('nst_conservation_is_frozen') && nst_conservation_is_frozen()) { return false; } $fh = fopen($CHAIN, 'ab+'); if (!$fh) return false; if (!flock($fh, LOCK_EX)) { fclose($fh); return false; } $ok = fwrite($fh, j($row) . "\n") !== false; fflush($fh); flock($fh, LOCK_UN); fclose($fh); return $ok; } /** * Exclusive chain read-modify-write. Holds LOCK_EX for the whole callback so * fill/faucet/transfer/order/admin_pay/extract/lazy_accrue races cannot double-spend. * Callback: fn(array $rows, callable(array):bool $append): mixed * Do NOT call append_row() inside — use $append only (same fd / non-deadlocking). * Returns null if the chain file cannot be opened/locked. */ function nst_with_chain_lock(callable $fn) { global $CHAIN; boot_data(); $fh = @fopen($CHAIN, 'c+b'); if (!$fh) return null; if (!flock($fh, LOCK_EX)) { fclose($fh); return null; } try { rewind($fh); $rows = []; while (($line = fgets($fh)) !== false) { $line = trim($line); if ($line === '') continue; $j = json_decode($line, true); if (is_array($j)) $rows[] = $j; } fseek($fh, 0, SEEK_END); $append = function (array $row) use ($fh, &$rows): bool { // E2 FREEZE: refuse append while sticky freeze file is present. if (function_exists('nst_conservation_is_frozen') && nst_conservation_is_frozen()) { return false; } $ok = fwrite($fh, j($row) . "\n") !== false; if ($ok) { fflush($fh); $rows[] = $row; } return $ok; }; return $fn($rows, $append); } finally { flock($fh, LOCK_UN); fclose($fh); } } /** * Exclusive file lock helper (durable files: rent_quotes, etc.). * Callback: fn(resource $fh): mixed — file opened c+b, pointer undefined. * Free claim faucet retired 410 — no cooldown file. */ function nst_with_file_lock(string $path, callable $fn) { $dir = dirname($path); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $fh = @fopen($path, 'c+b'); if (!$fh) return null; if (!flock($fh, LOCK_EX)) { fclose($fh); return null; } try { return $fn($fh); } finally { flock($fh, LOCK_UN); fclose($fh); } } function balances(array $rows): array { $b = []; foreach ($rows as $r) { $t = $r['type'] ?? ''; if ($t === 'genesis' || $t === 'treasury_reanchor') { foreach ($r['outputs'] ?? [] as $o) { $a = $o['addr'] ?? ''; $m = $o['amount'] ?? '0'; if ($a && dec_ok($m)) $b[$a] = dec_add($b[$a] ?? '0', $m); } // explicit treasury burn leg (excess liquid above 24_000_000 NSU KING target) if ($t === 'treasury_reanchor' && !empty($r['burn_amount']) && dec_ok((string)$r['burn_amount'])) { $ta = (string)($r['treasury_addr'] ?? ''); $bm = (string)$r['burn_amount']; if ($ta !== '' && dec_cmp($b[$ta] ?? '0', $bm) >= 0) { $b[$ta] = dec_sub($b[$ta] ?? '0', $bm); $burn = NST_BURN_ADDR; $b[$burn] = dec_add($b[$burn] ?? '0', $bm); } } } elseif ($t === 'transfer') { $f = $r['from'] ?? ''; $to = $r['to'] ?? ''; $m = $r['amount'] ?? '0'; if (!$f || !$to || !dec_ok($m)) continue; if (dec_cmp($b[$f] ?? '0', $m) < 0) continue; // skip invalid historical $b[$f] = dec_sub($b[$f] ?? '0', $m); $b[$to] = dec_add($b[$to] ?? '0', $m); } } return $b; } /* ======================================================================== * E2 CONSERVATION INVARIANT + FREEZE + int-micros audit (NSU-V1-SIMPLE STEP 1) * ADDITIVE ONLY. Does not alter balances()/transfer/faucet/order bodies. * On mismatch or non-integer money fields: sticky FREEZE file → all chain * writes reject → write APIs 503 conservation_freeze + loud red banner. * ======================================================================== */ /** True iff $m is a non-negative integer decimal string (micros). Same law as dec_ok. */ function nst_int_micros_ok(string $m): bool { return dec_ok($m); } /** * Audit money-bearing fields on chain rows for non-integer micros. * @return array{ok:bool,violations:list} */ function nst_audit_int_micros(array $rows): array { $violations = []; $check = static function (int $i, string $type, string $field, $raw) use (&$violations): void { if ($raw === null) { $violations[] = ['i' => $i, 'type' => $type, 'field' => $field, 'value' => '']; return; } $v = is_string($raw) || is_int($raw) ? (string)$raw : ''; if ($v === '' || !nst_int_micros_ok($v)) { $violations[] = ['i' => $i, 'type' => $type, 'field' => $field, 'value' => $v]; } }; foreach ($rows as $i => $r) { if (!is_array($r)) { continue; } $t = (string)($r['type'] ?? ''); if ($t === 'genesis' || $t === 'treasury_reanchor') { foreach ($r['outputs'] ?? [] as $j => $o) { if (!is_array($o)) { continue; } $check((int)$i, $t, 'outputs[' . $j . '].amount', $o['amount'] ?? null); } if ($t === 'treasury_reanchor' && array_key_exists('burn_amount', $r)) { $check((int)$i, $t, 'burn_amount', $r['burn_amount']); } } elseif ($t === 'transfer' || $t === 'fill') { if (array_key_exists('amount', $r)) { $check((int)$i, $t, 'amount', $r['amount']); } } elseif ($t === 'order') { if (array_key_exists('amount', $r)) { $check((int)$i, $t, 'amount', $r['amount']); } if (array_key_exists('price', $r)) { $check((int)$i, $t, 'price', $r['price']); } } } return ['ok' => $violations === [], 'violations' => $violations]; } /** * Replay conservation: expected issued vs Σ balances(). * Expected = genesis outputs + treasury_reanchor credit outputs only. * Burn stays in total (NST_BURN_ADDR). Transfers conserve. * * @return array{ok:bool,observed_sum:string,expected_issued:string,balances_sum:string,int_ok:bool,detail:array} */ function nst_conservation_replay(array $rows): array { $expected = '0'; $genesisN = 0; $genesisSum = '0'; $reanchorCreditN = 0; $reanchorCreditSum = '0'; foreach ($rows as $r) { if (!is_array($r)) { continue; } $t = (string)($r['type'] ?? ''); if ($t === 'genesis') { $genesisN++; foreach ($r['outputs'] ?? [] as $o) { if (!is_array($o)) { continue; } $m = (string)($o['amount'] ?? '0'); if (!nst_int_micros_ok($m)) { continue; } $expected = dec_add($expected, $m); $genesisSum = dec_add($genesisSum, $m); } } elseif ($t === 'treasury_reanchor') { foreach ($r['outputs'] ?? [] as $o) { if (!is_array($o)) { continue; } $m = (string)($o['amount'] ?? '0'); if (!nst_int_micros_ok($m) || dec_cmp($m, '0') <= 0) { continue; } $expected = dec_add($expected, $m); $reanchorCreditSum = dec_add($reanchorCreditSum, $m); $reanchorCreditN++; } } } $bal = balances($rows); $observed = '0'; $nonzeroAddrs = 0; foreach ($bal as $addr => $amt) { $a = (string)$amt; if (!nst_int_micros_ok($a)) { return [ 'ok' => false, 'observed_sum' => '0', 'expected_issued' => $expected, 'balances_sum' => '0', 'int_ok' => false, 'detail' => [ 'err' => 'balance_non_int', 'addr' => (string)$addr, 'amount' => $a, ], ]; } $observed = dec_add($observed, $a); if (dec_cmp($a, '0') > 0) { $nonzeroAddrs++; } } $intAudit = nst_audit_int_micros($rows); $match = dec_cmp($observed, $expected) === 0; $ok = $match && !empty($intAudit['ok']); $diff = '0'; if (!$match) { $diff = dec_cmp($observed, $expected) > 0 ? dec_sub($observed, $expected) : ('-' . dec_sub($expected, $observed)); } return [ 'ok' => $ok, 'observed_sum' => $observed, 'expected_issued' => $expected, 'balances_sum' => $observed, 'int_ok' => !empty($intAudit['ok']), 'detail' => [ 'match' => $match, 'diff' => $diff, 'genesis_rows' => $genesisN, 'genesis_sum' => $genesisSum, 'reanchor_credit_legs' => $reanchorCreditN, 'reanchor_credit_sum' => $reanchorCreditSum, 'nonzero_addrs' => $nonzeroAddrs, 'burn_addr' => NST_BURN_ADDR, 'burn_bal' => (string)($bal[NST_BURN_ADDR] ?? '0'), 'int_violations' => $intAudit['violations'], 'note' => 'expected=genesis+reanchor credits; burn moves to NST_BURN_ADDR (stays in total)', ], ]; } function nst_conservation_freeze_path(): string { global $DATA; boot_data(); return $DATA . DIRECTORY_SEPARATOR . 'conservation.freeze.json'; } /** @return array{frozen?:bool,ts?:int,reason?:string,observed?:string,expected?:string}|null */ function nst_conservation_freeze_read(): ?array { $p = nst_conservation_freeze_path(); if (!is_file($p)) { return null; } $j = json_decode((string)@file_get_contents($p), true); return is_array($j) ? $j : ['frozen' => true, 'reason' => 'unreadable_freeze_file']; } function nst_conservation_is_frozen(): bool { $j = nst_conservation_freeze_read(); if ($j === null) { return false; } // Fail closed: freeze file present with missing frozen key still blocks. if (array_key_exists('frozen', $j)) { return !empty($j['frozen']); } return true; } /** Sticky freeze until operator deletes file (no public unfreeze API). */ function nst_conservation_freeze_set(array $report, string $reason = 'conservation_mismatch'): bool { boot_data(); $payload = [ 'frozen' => true, 'ts' => time(), 'reason' => $reason, 'observed' => (string)($report['observed_sum'] ?? $report['observed'] ?? '0'), 'expected' => (string)($report['expected_issued'] ?? $report['expected'] ?? '0'), 'detail' => $report['detail'] ?? null, 'version' => NST_VERSION, ]; return @file_put_contents(nst_conservation_freeze_path(), j($payload), LOCK_EX) !== false; } /** Operator/test only — remove freeze file. Not auto-called on healthy check. */ function nst_conservation_freeze_clear(): bool { $p = nst_conservation_freeze_path(); if (!is_file($p)) { return true; } return @unlink($p); } /** * Run conservation replay; FREEZE on mismatch or int-micros fail. * Sticky: never auto-clears freeze (silent heal forbidden). */ function nst_conservation_check_and_maybe_freeze(array $rows): array { $rep = nst_conservation_replay($rows); $rep['checked_ts'] = time(); $rep['frozen'] = nst_conservation_is_frozen(); $rep['freeze_written'] = false; if (empty($rep['ok'])) { $why = empty($rep['int_ok']) ? 'int_micros_violation' : 'conservation_mismatch'; $rep['freeze_written'] = nst_conservation_freeze_set($rep, $why); $rep['frozen'] = true; } // Best-effort meta stamp (freeze file is authority). $m = load_meta(); $m['last_conservation_ts'] = (int)$rep['checked_ts']; $m['last_conservation_ok'] = !empty($rep['ok']); $m['last_conservation_observed'] = (string)$rep['observed_sum']; $m['last_conservation_expected'] = (string)$rep['expected_issued']; save_meta($m); return $rep; } /** Early reject for write APIs. 503 + err conservation_freeze. */ function nst_require_writes_unfrozen(): void { if (!nst_conservation_is_frozen()) { return; } $fr = nst_conservation_freeze_read() ?? []; api_out([ 'ok' => false, 'err' => 'conservation_freeze', 'frozen' => true, 'reason' => (string)($fr['reason'] ?? 'conservation_mismatch'), 'observed_fmt' => isset($fr['observed']) && nst_int_micros_ok((string)$fr['observed']) ? fmt_amt((string)$fr['observed']) : null, 'expected_fmt' => isset($fr['expected']) && nst_int_micros_ok((string)$fr['expected']) ? fmt_amt((string)$fr['expected']) : null, 'freeze_ts' => (int)($fr['ts'] ?? 0), 'note' => 'WRITES FROZEN — ledger conservation check failed on this host. Transfers, claims, and book posts are paused until the chain balances match again. Read-only view still works. Experimental rails — not insured, not a bank.', ], 503); } /** * Throttled boot check. Always honors freeze file. * @return array public-safe conservation snapshot */ function nst_conservation_boot_check(array $rows, bool $force = false): array { $frozen = nst_conservation_is_frozen(); $meta = load_meta(); $last = (int)($meta['last_conservation_ts'] ?? 0); $age = $last > 0 ? (time() - $last) : PHP_INT_MAX; $need = $force || $last < 1 || $age >= NST_CONSERVATION_RECHECK_SECS; if ($frozen && !$force) { $fr = nst_conservation_freeze_read() ?? []; return [ 'ok' => false, 'frozen' => true, 'sum_fmt' => isset($fr['observed']) && nst_int_micros_ok((string)$fr['observed']) ? fmt_amt((string)$fr['observed']) : null, 'expected_fmt' => isset($fr['expected']) && nst_int_micros_ok((string)$fr['expected']) ? fmt_amt((string)$fr['expected']) : null, 'checked_ts' => (int)($fr['ts'] ?? $last), 'throttled' => true, 'reason' => (string)($fr['reason'] ?? 'conservation_freeze'), ]; } if (!$need && !$frozen) { $obs = (string)($meta['last_conservation_observed'] ?? ''); $exp = (string)($meta['last_conservation_expected'] ?? ''); $okMeta = !empty($meta['last_conservation_ok']); if ($obs !== '' && $exp !== '' && nst_int_micros_ok($obs) && nst_int_micros_ok($exp)) { return [ 'ok' => $okMeta, 'frozen' => false, 'sum_fmt' => fmt_amt($obs), 'expected_fmt' => fmt_amt($exp), 'checked_ts' => $last, 'throttled' => true, ]; } } $rep = nst_conservation_check_and_maybe_freeze($rows); return [ 'ok' => !empty($rep['ok']), 'frozen' => !empty($rep['frozen']), 'sum_fmt' => fmt_amt((string)$rep['observed_sum']), 'expected_fmt' => fmt_amt((string)$rep['expected_issued']), 'checked_ts' => (int)$rep['checked_ts'], 'throttled' => false, 'int_ok' => !empty($rep['int_ok']), 'detail' => $rep['detail'] ?? null, ]; } /** Public-facing conservation object for api=state / HTML. */ function nst_conservation_public_view(?array $bootSnap = null): array { if (is_array($bootSnap)) { return [ 'ok' => !empty($bootSnap['ok']), 'frozen' => !empty($bootSnap['frozen']) || nst_conservation_is_frozen(), 'sum_fmt' => $bootSnap['sum_fmt'] ?? null, 'expected_fmt' => $bootSnap['expected_fmt'] ?? null, 'checked_ts' => (int)($bootSnap['checked_ts'] ?? 0), ]; } $frozen = nst_conservation_is_frozen(); $fr = $frozen ? (nst_conservation_freeze_read() ?? []) : []; $meta = load_meta(); $obs = (string)($fr['observed'] ?? $meta['last_conservation_observed'] ?? ''); $exp = (string)($fr['expected'] ?? $meta['last_conservation_expected'] ?? ''); return [ 'ok' => !$frozen && !empty($meta['last_conservation_ok']), 'frozen' => $frozen, 'sum_fmt' => ($obs !== '' && nst_int_micros_ok($obs)) ? fmt_amt($obs) : null, 'expected_fmt' => ($exp !== '' && nst_int_micros_ok($exp)) ? fmt_amt($exp) : null, 'checked_ts' => (int)($fr['ts'] ?? $meta['last_conservation_ts'] ?? 0), ]; } /** * World population metadata only (not used for supply scale). * Liquid genesis / re-anchor = fixed 24_000_000 NSU KING faucet. * Fail-open sources: * 1) data/world_pop.override (operator integer — durable, not ephemeral) * 2) ephemeral world_pop.json cache if fresh (prefer /dev/shm; else data/ephemeral/) * 3) crawl allowlisted public stats (World Bank WLD SP.POP.TOTL) * 4) NST_POP_FALLBACK constant * Never blocks wipe/genesis if network dies — fixed 24_000_000 NSU still lands. * Cache is regenerable L3 only; losing it triggers recompute — never money SoT. * * @return array{pop:int,source:string,ts:int,cached:bool} */ function nst_world_pop_cache_path(): string { return nst_ephemeral_dir() . DIRECTORY_SEPARATOR . 'world_pop.json'; } function nst_world_population(): array { global $DATA; boot_data(); $override = $DATA . DIRECTORY_SEPARATOR . 'world_pop.override'; if (is_file($override)) { $n = (int)trim((string)@file_get_contents($override)); if ($n >= 1000000 && $n <= 50000000000) { return ['pop' => $n, 'source' => 'override', 'ts' => time(), 'cached' => false]; } } $cache = nst_world_pop_cache_path(); if (is_file($cache)) { $j = json_decode((string)@file_get_contents($cache), true); if (is_array($j) && !empty($j['pop']) && !empty($j['ts'])) { $age = time() - (int)$j['ts']; if ($age >= 0 && $age < NST_POP_CACHE_SECS && (int)$j['pop'] >= 1000000) { return [ 'pop' => (int)$j['pop'], 'source' => (string)($j['source'] ?? 'cache'), 'ts' => (int)$j['ts'], 'cached' => true, ]; } } } $crawled = nst_crawl_world_population(); if ($crawled !== null) { $edir = dirname($cache); if (!is_dir($edir)) { @mkdir($edir, 0755, true); } @file_put_contents($cache, j($crawled), LOCK_EX); return $crawled + ['cached' => false]; } return [ 'pop' => NST_POP_FALLBACK, 'source' => 'fallback_constant', 'ts' => time(), 'cached' => false, ]; } /** @return array{pop:int,source:string,ts:int}|null */ function nst_crawl_world_population(): ?array { if (!ini_get('allow_url_fopen') && !function_exists('curl_init')) { return null; } // World Bank: total population, world aggregate, most recent value $url = 'https://api.worldbank.org/v2/country/WLD/indicator/SP.POP.TOTL?format=json&per_page=5&mrv=5'; $raw = nst_http_get_limited($url, 6); if ($raw === null || $raw === '') return null; $j = json_decode($raw, true); if (!is_array($j) || !isset($j[1]) || !is_array($j[1])) return null; foreach ($j[1] as $row) { if (!is_array($row)) continue; $v = $row['value'] ?? null; if ($v === null || $v === '') continue; $n = (int)$v; if ($n >= 1000000000 && $n <= 50000000000) { return [ 'pop' => $n, 'source' => 'worldbank:SP.POP.TOTL', 'ts' => time(), ]; } } return null; } function nst_http_get_limited(string $url, int $timeoutSec): ?string { $host = parse_url($url, PHP_URL_HOST); if (!is_string($host) || !preg_match('/^(api\.worldbank\.org)$/i', $host)) { return null; // allowlist only - no open SSRF } if (function_exists('curl_init')) { $ch = curl_init($url); if ($ch === false) return null; curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => $timeoutSec, CURLOPT_TIMEOUT => $timeoutSec, CURLOPT_USERAGENT => 'nosignup.trade-pop/1.0', CURLOPT_SSL_VERIFYPEER => true, ]); $body = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false || $code >= 400) return null; return (string)$body; } $ctx = stream_context_create([ 'http' => [ 'timeout' => $timeoutSec, 'header' => "User-Agent: nosignup.trade-pop/1.0\r\n", ], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true], ]); $body = @file_get_contents($url, false, $ctx); return $body === false ? null : (string)$body; } /** * Optimum liquid treasury micros = fixed KING faucet start (24_000_000 NSU). * $pop retained for API compatibility / metadata; not used for supply scale. */ function nst_optimum_treasury_micros(int $pop = 0): string { $m = parse_amt((string)(int)NST_KING_FAUCET_START_NSU); if ($m !== null && dec_ok($m) && dec_cmp($m, '0') > 0) { return $m; } return NST_GENESIS_TREASURY; } /** * After wipe (or first genesis): set treasury liquid to 24_000_000 NSU target. * - If treasury below target → credit (epoch mint / top-up) - not a continuous faucet mint * - If treasury above target → burn excess from treasury only * User balances never touched. Soft max_supply soft-updates to max(legacy, target). * * @return array{pop:int,source:string,target:string,before:string,after:string,action:string} */ function nst_treasury_reanchor_to_population(string $reason): array { ensure_genesis_secret_only(); $sec = trim((string)file_get_contents($GLOBALS['TFILE'])); $taddr = addr_from_seed($sec); $popInfo = nst_world_population(); $pop = (int)$popInfo['pop']; $target = nst_optimum_treasury_micros($pop); $bal = balances(read_chain()); $before = (string)($bal[$taddr] ?? '0'); $ts = time(); $action = 'hold'; $row = [ 'type' => 'treasury_reanchor', 'ver' => 2, 'ts' => $ts, 'reason' => $reason, 'population' => $pop, 'population_source' => (string)$popInfo['source'], 'faucet_start_nsu' => NST_KING_FAUCET_START_NSU, 'target_micros' => $target, 'target_fmt' => fmt_amt($target), 'treasury_before_fmt' => fmt_amt($before), 'treasury_addr' => $taddr, 'outputs' => [], 'burn_amount' => '0', 'note' => 'treasury re-anchor: liquid KING faucet targets 24_000_000 NSU; user balances untouched', ]; $cmp = dec_cmp($before, $target); if ($cmp < 0) { $delta = dec_sub($target, $before); $row['outputs'] = [['addr' => $taddr, 'amount' => $delta]]; $row['action'] = 'credit'; $row['delta_fmt'] = fmt_amt($delta); $action = 'credit'; } elseif ($cmp > 0) { $delta = dec_sub($before, $target); $row['burn_amount'] = $delta; $row['action'] = 'burn'; $row['delta_fmt'] = fmt_amt($delta); $action = 'burn'; } else { $row['action'] = 'hold'; $row['delta_fmt'] = '0'; } $body = 'reanchor|' . $reason . '|' . $pop . '|' . $target . '|' . $before . '|' . $action . '|' . $ts; $row['sig'] = mac_sign($sec, $body); if ($action !== 'hold') { append_row($row); } else { // still log hold for audit (no balance change) append_row($row); } $afterBal = balances(read_chain()); $after = (string)($afterBal[$taddr] ?? '0'); $m = load_meta(); $m['population'] = $pop; $m['population_source'] = (string)$popInfo['source']; $m['population_ts'] = (int)$popInfo['ts']; $m['treasury_target_micros'] = $target; $m['treasury_reanchor_ts'] = $ts; // soft cap display: KING faucet target (24_000_000 NSU fixed product scale) $m['soft_max_supply_micros'] = $target; save_meta($m); return [ 'pop' => $pop, 'source' => (string)$popInfo['source'], 'target' => $target, 'before' => $before, 'after' => $after, 'action' => $action, ]; } /** * Generate a 12-word site-local seed (same product surface as user wallets). * Used for: (KING) treasury/master faucet at genesis; (LORD) operator panel seed on rent. * KING seed never goes to renters. LORD seed never unlocks faucet/mint. */ function nst_generate_site_seed(): string { // 12-word site seed (not BIP39). Product surface matches user wallets. // Word pool is small + high entropy bytes — site-local only. static $wl = [ 'able','acid','aged','also','aqua','arch','area','army','atom','aunt','auto','avoid', 'axis','baby','band','bank','bare','barn','base','bean','bear','belt','bike','bind', 'bird','bite','blue','boat','body','bold','bolt','bone','book','boot','born','bowl', 'brass','brave','bread','brick','brief','bring','broad','broke','brown','brush','build','bulk', 'burn','burst','bush','busy','cable','cage','cake','calm','camp','cane','cape','card', 'care','cart','case','cash','cast','cave','cell','cent','chat','chef','chin','chip', 'city','clap','clay','clip','club','coal','coat','code','coil','coin','cold','come', 'cook','cool','cope','copy','cord','core','corn','cost','cove','crab','crew','crop', 'crow','cube','cult','curb','cure','curl','dark','dart','dash','data','dawn','deal', 'dear','deck','deep','deer','desk','dial','dice','diet','dine','dirt','disc','dock', 'dome','done','door','dose','down','draw','drip','drop','drum','dual','duck','dune', 'dusk','dust','duty','each','earn','east','easy','echo','edge','edit','else','emit', 'epic','even','ever','evil','exit','face','fact','fade','fail','fair','fall','fame', 'farm','fast','fate','fear','feed','feel','fern','file','fill','film','find','fine', 'fire','firm','fish','flag','flat','flee','flip','flow','foam','foil','fold','font', 'food','fool','foot','ford','fork','form','fort','foul','four','free','frog','from', 'fuel','full','fund','fuse','gain','game','gate','gear','gene','gift','girl','give', 'glad','glow','glue','goal','goat','gold','golf','good','grab','grad','gram','gray', 'grid','grim','grin','grip','grow','gulf','guru','hail','hair','half','hall','hand', 'hang','hard','harm','harp','hate','have','hawk','haze','head','heal','heap','heat', 'heed','heel','held','help','herb','here','hero','hide','high','hill','hint','hire', 'hold','hole','home','hood','hook','hope','horn','host','hour','huge','hull','hung', 'hunt','hurt','icon','idea','idle','inch','info','into','iron','item','jade','jail', 'jazz','join','joke','jump','june','jury','just','keen','keep','kept','kick','kind', 'king','kite','knee','knew','knit','knot','know','lace','lack','lady','lake','lamp', 'land','lane','last','late','lava','lawn','lead','leaf','lean','left','lend','lens', ]; $n = count($wl); $bytes = random_bytes(12); $out = []; for ($i = 0; $i < 12; $i++) { $out[] = $wl[ord($bytes[$i]) % $n]; } return implode(' ', $out); } /** * Shared vault note writer — file body is caller-owned; never mixes KING/SITE/ADMIN secrets. * Each role builds its own $body; this only ensures dir + 0600 write. */ function nst_vault_write_note(string $filename, string $body): void { $d = vault_dir(); if (!is_dir($d) && !@mkdir($d, 0700, true)) { return; } @chmod($d, 0700); $path = $d . DIRECTORY_SEPARATOR . $filename; @file_put_contents($path, $body, LOCK_EX); @chmod($path, 0600); } /** * Vault note for KING: data/treasury.secret only. Never to renters/lords. * E3: distinct from trade panel site.seed (11-wallet: KING is the 11th, only minter). */ function nst_vault_king_seed_note(string $seed): void { $body = "NOSIGNUP.TRADE KING / MASTER FAUCET SEED (OWNER ONLY — HIDDEN VAULT)\n" . "Role: empire KING vault wallet. ONLY minter for THIS trade HQ.\n" . "File: data/treasury.secret · Unlocks faucet/mint (require_faucet_auth).\n" . "Also unlocks /controlpanel as owner override. Lords NEVER receive this.\n" . "Genesis / re-anchor liquid = " . NST_KING_FAUCET_START_NSU . " NSU. NO RECOVERY.\n" . "11-wallet model: this is the 11th seed (hidden KING), not a site panel.\n" . "Generated: " . gmdate('c') . "\n\n" . trim($seed) . "\n"; nst_vault_write_note('KING_FAUCET_SEED.txt', $body); } /** @deprecated name kept: writes KING vault note (not panel). */ function nst_vault_site_seed_note(string $seed): void { nst_vault_king_seed_note($seed); } /** * Vault note for trade SITE PANEL wallet (one of 10 crop panels). Not a mint. */ function nst_vault_panel_seed_note(string $seed): void { $body = "NOSIGNUP.TRADE SITE PANEL WALLET SEED (THIS CROP ONLY)\n" . "Role: /controlpanel unlock + site operator wallet for trade.\n" . "File: data/site.seed · Does NOT mint. Lords never mint; KING alone mints.\n" . "After public rent pay: economy.operator_addr may become the LORD payer key.\n" . "Generated: " . gmdate('c') . "\n\n" . trim($seed) . "\n"; nst_vault_write_note('SITE_WALLET_SEED.txt', $body); } /** * Ensure trade panel site.seed exists (distinct from KING treasury.secret on fresh genesis). * Do not invent a second key on pre-E3 installs that already genesi'd without site.seed — * only call from the fresh-genesis path. */ function ensure_panel_site_seed_only(): string { global $SITE_SEED_FILE; boot_data(); if (!is_file($SITE_SEED_FILE) || trim((string)@file_get_contents($SITE_SEED_FILE)) === '') { $seed = nst_generate_site_seed(); file_put_contents($SITE_SEED_FILE, $seed, LOCK_EX); @chmod($SITE_SEED_FILE, 0600); nst_vault_panel_seed_note($seed); return $seed; } $seed = trim((string)@file_get_contents($SITE_SEED_FILE)); if ($seed !== '') { nst_vault_panel_seed_note($seed); } return $seed; } /** * On fresh genesis: operator.addr = panel site.seed addr (≠ KING treasury). * File is public-ish addr only (not secret). Economy.operator_addr may diverge for rent later. * Legacy (no site.seed): falls back to treasury addr so pre-E3 installs keep working. */ function nst_ensure_operator_addr(?string $addr = null): string { global $OP_ADDR_FILE, $SITE_SEED_FILE, $TFILE; boot_data(); if ($addr === null || $addr === '') { if (is_file($OP_ADDR_FILE)) { $have = strtolower(trim((string)@file_get_contents($OP_ADDR_FILE))); if (preg_match('/^[a-f0-9]{64}$/', $have)) { return $have; } } // Prefer panel site.seed (E3 distinct panel wallet); else KING treasury (legacy conflation). if (isset($SITE_SEED_FILE) && is_file($SITE_SEED_FILE)) { $psec = trim((string)@file_get_contents($SITE_SEED_FILE)); if ($psec !== '') { $addr = addr_from_seed($psec); } } if (($addr === null || $addr === '') && is_file($TFILE)) { $sec = trim((string)@file_get_contents($TFILE)); if ($sec !== '') { $addr = addr_from_seed($sec); } } } if ($addr === null || $addr === '' || !preg_match('/^[a-f0-9]{64}$/', $addr)) { return ''; } $addr = strtolower($addr); if (!is_file($OP_ADDR_FILE) || trim((string)@file_get_contents($OP_ADDR_FILE)) === '') { file_put_contents($OP_ADDR_FILE, $addr . "\n", LOCK_EX); @chmod($OP_ADDR_FILE, 0644); } return $addr; } /** * Panel unlock addresses (E3): * - KING treasury seed (owner override; still not a lord mint path) * - trade panel site.seed (site admin door; no mint) * - economy.operator_addr (LORD after rent pay-bind) * Never ship treasury.secret to renters. * * @return list */ function panel_unlock_addrs(): array { $out = []; global $TFILE, $SITE_SEED_FILE; if (is_file($TFILE)) { $sec = trim((string)@file_get_contents($TFILE)); if ($sec !== '') { $out[] = addr_from_seed($sec); } } if (isset($SITE_SEED_FILE) && is_file($SITE_SEED_FILE)) { $psec = trim((string)@file_get_contents($SITE_SEED_FILE)); if ($psec !== '') { $pa = addr_from_seed($psec); if (!in_array($pa, $out, true)) { $out[] = $pa; } } } $e = load_economy(); $op = strtolower(trim((string)($e['operator_addr'] ?? ''))); if (preg_match('/^[a-f0-9]{64}$/', $op) && !in_array($op, $out, true)) { // rent path: LORD payer key unlocks panel without treasury secret $out[] = $op; } return $out; } /** True if seed derives KING, site panel, or economy.operator_addr (LORD). */ function panel_seed_ok(string $seed): bool { $seed = norm_seed($seed); if ($seed === '') { return false; } $addr = addr_from_seed($seed); foreach (panel_unlock_addrs() as $a) { if (hash_equals($a, $addr)) { return true; } } return false; } /** True if seed derives the treasury master faucet (KING) only — not LORD operator. */ function treasury_seed_ok(string $seed): bool { $seed = norm_seed($seed); if ($seed === '') { return false; } global $TFILE; if (!is_file($TFILE)) { return false; } $sec = trim((string)@file_get_contents($TFILE)); if ($sec === '') { return false; } return hash_equals(addr_from_seed($sec), addr_from_seed($seed)); } /** * Faucet / mint authority (KING): treasury seed ONLY. * LORD operator seed never unlocks faucet, treasury reveal, free-form pay, rent claim, * or profit extract. Legacy admin.pass.hash may still unlock the *panel* via require_admin * (one-release migrate), but MUST NOT mint — a renter who can set/change the panel password * would otherwise escalate to faucet (isolation hole closed 2026-07-15). */ function require_faucet_auth(): void { $seed = (string)($_POST['seed'] ?? ''); if ($seed !== '' && treasury_seed_ok($seed)) { return; } api_out(['ok' => false, 'err' => 'faucet auth'], 401); } /** * DEAD — former market-value LORD rent GRANT compute (treasury → LORD). * Kept only so old call sites / tests that reference the symbol do not fatals until full prune. * Product rent must NOT call this for live settlement (admin_rent_claim is 410). * @param string $treasuryMicros remaining treasury balance (micros) * @param string $lastProfitMicros prior epoch profit pile at wipe (micros), or '0' * @return array{ok:bool,micros?:string,credit_nsu?:string,parts?:array,err?:string} */ function nst_lord_rent_credit_compute(string $treasuryMicros, string $lastProfitMicros = '0'): array { $treasuryMicros = dec_norm($treasuryMicros); $lastProfitMicros = dec_norm($lastProfitMicros); if (!dec_ok($treasuryMicros) || !dec_ok($lastProfitMicros)) { return ['ok' => false, 'err' => 'bad amounts']; } // percent of remaining faucet $pctPart = '0'; if (LORD_RENT_PCT_BPS > 0 && dec_cmp($treasuryMicros, '0') > 0) { $pctPart = dec_div_small(dec_mul_small($treasuryMicros, (int)LORD_RENT_PCT_BPS), 10000); } $flatBase = parse_amt((string)(int)LORD_RENT_FLAT_BASE_NSU); if ($flatBase === null) { return ['ok' => false, 'err' => 'flat base invalid']; } $perfPart = '0'; if (LORD_RENT_FLAT_PERF_BPS > 0 && dec_cmp($lastProfitMicros, '0') > 0) { $perfPart = dec_div_small(dec_mul_small($lastProfitMicros, (int)LORD_RENT_FLAT_PERF_BPS), 10000); $cap = parse_amt((string)(int)LORD_RENT_FLAT_PERF_CAP_NSU); if ($cap !== null && dec_cmp($perfPart, $cap) > 0) { $perfPart = $cap; } } $total = dec_add(dec_add($pctPart, $flatBase), $perfPart); $minM = parse_amt((string)(int)LORD_RENT_MIN_NSU); $maxM = parse_amt((string)(int)LORD_RENT_MAX_NSU); if ($minM !== null && dec_cmp($total, $minM) < 0) { $total = $minM; } if ($maxM !== null && dec_cmp($total, $maxM) > 0) { $total = $maxM; } if (dec_cmp($total, '1') < 0) { return ['ok' => false, 'err' => 'credit too small']; } // whole NSU display (floor micros / 1e6) $creditNsu = dec_div_small($total, (int)str_pad('1', NST_DECIMALS + 1, '0')); return [ 'ok' => true, 'micros' => $total, 'credit_nsu' => $creditNsu, 'parts' => [ 'pct_bps' => LORD_RENT_PCT_BPS, 'pct_micros' => $pctPart, 'pct_fmt' => fmt_amt($pctPart), 'flat_base_nsu' => LORD_RENT_FLAT_BASE_NSU, 'flat_base_fmt' => fmt_amt($flatBase), 'flat_perf_bps' => LORD_RENT_FLAT_PERF_BPS, 'flat_perf_micros' => $perfPart, 'flat_perf_fmt' => fmt_amt($perfPart), 'last_profit_fmt' => fmt_amt($lastProfitMicros), 'treasury_fmt' => fmt_amt($treasuryMicros), 'min_nsu' => LORD_RENT_MIN_NSU, 'max_nsu' => LORD_RENT_MAX_NSU, 'legacy_flat_placeholder' => LORD_RENT_CREDIT_LEGACY, 'faucet_start_nsu_narrative' => NST_KING_FAUCET_START_NSU, ], ]; } /** @deprecated prefer nst_lord_rent_credit_compute with live treasury */ function nst_lord_rent_credit_micros(): ?string { $c = nst_lord_rent_credit_compute('0', '0'); return !empty($c['ok']) ? (string)$c['micros'] : null; } /** Ensure KING treasury.secret exists without double-genesis. New installs get 12-word KING seed. */ function ensure_genesis_secret_only(): void { global $TFILE; boot_data(); if (!is_file($TFILE) || trim((string)@file_get_contents($TFILE)) === '') { $seed = nst_generate_site_seed(); file_put_contents($TFILE, $seed, LOCK_EX); @chmod($TFILE, 0600); nst_vault_site_seed_note($seed); } } function fill_amounts(array $rows): array { $filled = []; foreach ($rows as $r) { if (($r['type'] ?? '') === 'fill' && !empty($r['order_id'])) { $filled[$r['order_id']] = dec_add($filled[$r['order_id']] ?? '0', $r['amount'] ?? '0'); } } return $filled; } function open_orders(array $rows): array { $live = []; $cancelled = []; $filled = fill_amounts($rows); foreach ($rows as $r) { $t = $r['type'] ?? ''; if ($t === 'order') $live[$r['id']] = $r; if ($t === 'cancel' && !empty($r['order_id'])) $cancelled[$r['order_id']] = true; } $now = time(); $out = []; foreach ($live as $id => $o) { if (!empty($cancelled[$id])) continue; if (($o['expiry'] ?? 0) < $now) continue; $rem = dec_sub($o['amount'] ?? '0', $filled[$id] ?? '0'); if (dec_cmp($rem, '1') < 0) continue; $o['remaining'] = $rem; $o['remaining_fmt'] = fmt_amt($rem); $out[] = $o; } usort($out, function ($a, $b) { $c = dec_cmp($a['price'] ?? '0', $b['price'] ?? '0'); if (($a['side'] ?? '') === 'buy') $c = -$c; // buys high first if ($c !== 0) return $c; return ($a['ts'] ?? 0) <=> ($b['ts'] ?? 0); }); return $out; } /** NSU locked in open sell orders (pair starts with NSU) */ function reserved_nsu(array $rows, string $addr): string { $sum = '0'; foreach (open_orders($rows) as $o) { if (($o['addr'] ?? '') !== $addr) continue; if (($o['side'] ?? '') !== 'sell') continue; $pair = strtoupper($o['pair'] ?? ''); if (!str_starts_with($pair, 'NSU')) continue; $sum = dec_add($sum, $o['remaining'] ?? '0'); } return $sum; } function available_nsu(array $bal, array $rows, string $addr): string { $have = $bal[$addr] ?? '0'; $res = reserved_nsu($rows, $addr); $av = dec_sub($have, $res); return dec_cmp($av, '0') < 0 ? '0' : $av; } function rep_scores(array $rows): array { // latest edge per (rater,rated) $edges = []; foreach ($rows as $r) { if (($r['type'] ?? '') !== 'reputation') continue; $k = ($r['rater'] ?? '') . '>' . ($r['rated'] ?? ''); $prev = $edges[$k] ?? null; if (!$prev || ($r['ts'] ?? 0) >= ($prev['ts'] ?? 0)) $edges[$k] = $r; } $score = []; foreach ($edges as $e) { $rated = $e['rated'] ?? ''; $s = (int)($e['score'] ?? 0); if ($rated === '' || $s < -10 || $s > 10) continue; $score[$rated] = ($score[$rated] ?? 0) + $s; } return $score; } /* ---- meta / epochs / modifiers / admin (data/*.txt inaccessible via .htaccess) ---- */ function load_meta(): array { global $META; if (!is_file($META)) return []; $j = json_decode((string)file_get_contents($META), true); return is_array($j) ? $j : []; } function save_meta(array $m): void { global $META; file_put_contents($META, j($m), LOCK_EX); } /** Site-local economy: profit pile + ad bid stake (not empire-wide). */ function load_economy(): array { global $ECON; $def = [ 'profit_micros' => '0', 'ad_bid_micros' => '0', 'donate_k' => NST_DONATE_K, 'value_note' => '', 'bids' => [], 'donations' => [], ]; if (!is_file($ECON)) return $def; $j = json_decode((string)file_get_contents($ECON), true); if (!is_array($j)) return $def; return array_merge($def, $j); } function save_economy(array $e): void { global $ECON; file_put_contents($ECON, j($e), LOCK_EX); } function ads_dir(): string { global $ADS; if (!is_dir($ADS)) @mkdir($ADS, 0755, true); $ht = $ADS . DIRECTORY_SEPARATOR . '.htaccess'; // creatives served only via ?api=ad_img - deny direct if under web (belt) if (!is_file($ht)) @file_put_contents($ht, "Require all denied\nDeny from all\n"); return $ADS; } /** * Yearly reset wipe (product intent = Chinese New Year epoch boundary). * ONLY user NSU wallet balances survive (chain history). * Superstructure is burned so a leaked/brute-forced panel password can only * steal for the remainder of the epoch - not forever. * * CNY / epoch inferences (operator 2026-07-15): * - Rent is an epoch lease: LORD operator seed + rented flag die at wipe. * - KING faucet re-anchors; next rent grant uses remaining faucet + last-year profit. * - User balances survive (no seizure); open orders / ads / profit piles do not. * - Password product path (if any residual) is re-keyed; seed-panel is product door. * * Wiped: profit pile, ad stakes/bids, ad creatives, donate credit log, rent lease. * Not wiped: transfer ledger (balances). Real-chain vault seeds stay offline * (operator must move external funds; site does not auto-sweep chains). * Admin panel password: AUTOMATICALLY rotated on wipe so leaked passwords die * at year boundary; new cleartext lands in vault/ only. */ function economy_on_epoch_roll(int $fromN, int $toN): void { $prev = load_economy(); $oldProfit = (string)($prev['profit_micros'] ?? '0'); $oldAd = (string)($prev['ad_bid_micros'] ?? '0'); $oldBids = is_array($prev['bids'] ?? null) ? count($prev['bids']) : 0; $oldLordCredit = (string)($prev['lord_credit_micros'] ?? '0'); // burn site-local superstructure; end renter lease; stash last-year performance $e = [ 'profit_micros' => '0', 'ad_bid_micros' => '0', 'donate_k' => (string)($prev['donate_k'] ?? NST_DONATE_K), 'bids' => [], 'donations' => [], 'epoch_n' => $toN, 'rolled_from' => $fromN, 'roll_ts' => time(), 'wipe' => 'balances_only_survive', 'wipe_calendar' => 'chinese_new_year_epoch', // CNY: renter lease ends — new year needs new purchase + new LORD seed 'rented' => false, 'operator_addr' => '', 'rented_at' => 0, 'lord_credit_micros' => '0', // performance inputs for next rent grant formula 'last_epoch_profit_micros' => $oldProfit, 'last_epoch_ad_bid_micros' => $oldAd, 'last_epoch_lord_credit_micros' => $oldLordCredit, 'last_epoch_n' => $fromN, ]; save_economy($e); // clear public operator.addr file so panel falls back to KING treasury until re-rent global $OP_ADDR_FILE; if (isset($OP_ADDR_FILE) && is_file($OP_ADDR_FILE)) { @file_put_contents($OP_ADDR_FILE, '', LOCK_EX); } // delete ad PNGs under data/ads/ $dir = ads_dir(); foreach (glob($dir . DIRECTORY_SEPARATOR . '*.png') ?: [] as $f) { @unlink($f); } // cancel open orders so book does not survive as superstructure $ts = time(); $nCancel = 0; foreach (open_orders(read_chain()) as $o) { $oid = (string)($o['id'] ?? ''); $addr = (string)($o['addr'] ?? ''); if ($oid === '' || $addr === '') continue; append_row([ 'type' => 'cancel', 'ver' => 1, 'order_id' => $oid, 'addr' => $addr, 'ts' => $ts, 'sig' => 'year_wipe', 'auth' => 'epoch_roll', 'note' => 'yearly wipe - open orders do not survive', ]); $nCancel++; } // permanent early modifiers do not survive - only balances global $MODS_FILE; if (is_file($MODS_FILE)) { @file_put_contents($MODS_FILE, '{}', LOCK_EX); } // Wallet Inbox v0: sealed notices burn with superstructure (balances survive). nst_inbox_burn_all(); // kill leaked panel passwords: mint new admin key into vault $newPass = bin2hex(random_bytes(12)); $rotated = false; try { // set hash + vault without api_out (inline) global $ADMIN_HASH_FILE, $ADMIN_PASS_FILE; $hash = password_hash($newPass, PASSWORD_DEFAULT); if ($hash !== false) { file_put_contents($ADMIN_HASH_FILE, $hash . "\n", LOCK_EX); @chmod($ADMIN_HASH_FILE, 0600); if (is_file($ADMIN_PASS_FILE)) @unlink($ADMIN_PASS_FILE); vault_write_operator($newPass); $rotated = true; @file_put_contents( vault_dir() . DIRECTORY_SEPARATOR . 'YEAR_WIPE.txt', "Year wipe epoch $fromN -> $toN at " . gmdate('c') . "\n" . "New admin password in ADMIN_PASSWORD.txt\n" . "Old panel passwords are dead.\n" . "ONLY user NSU balances survived on the ledger.\n", LOCK_EX ); } } catch (Throwable $ex) { $rotated = false; } // Liquid restart: treasury → fixed 24_000_000 NSU KING faucet target. Users keep coins. $anchor = ['action' => 'skipped', 'pop' => 0, 'source' => '', 'target' => '0', 'before' => '0', 'after' => '0']; try { $anchor = nst_treasury_reanchor_to_population('year_wipe_epoch_' . $fromN . '_to_' . $toN); } catch (Throwable $ex) { $anchor['action'] = 'error'; } // Book was wiped empty — re-seed loose discovery spread (no live orders to protect). $boot = ['ok' => false]; try { $boot = nst_bootstrap_spread_if_empty(); } catch (Throwable $ex) { $boot = ['ok' => false, 'err' => 'bootstrap_exception']; } append_row([ 'type' => 'economy_year_wipe', 'ver' => 2, 'ts' => $ts, 'from_epoch' => $fromN, 'to_epoch' => $toN, 'wiped_profit_fmt' => fmt_amt($oldProfit), 'wiped_ad_bid_fmt' => fmt_amt($oldAd), 'wiped_bids' => $oldBids, 'orders_cancelled' => $nCancel, 'admin_rotated' => $rotated, 'population' => $anchor['pop'] ?? 0, 'population_source' => $anchor['source'] ?? '', 'treasury_target_fmt' => isset($anchor['target']) ? fmt_amt((string)$anchor['target']) : null, 'treasury_reanchor' => $anchor['action'] ?? null, 'treasury_after_fmt' => isset($anchor['after']) ? fmt_amt((string)$anchor['after']) : null, 'bootstrap_spread' => !empty($boot['ok']) && empty($boot['skipped']), 'bootstrap_note' => $boot['reason'] ?? ($boot['amount_fmt'] ?? null), 'siphon_note' => 'profit/ad superstructure burns into re-anchor path; renters never mint; user balances only survive', 'note' => 'yearly reset: ONLY wallet balances survive; superstructure wiped (rent siphon/burn into re-anchor); admin re-keyed; treasury re-anchored; bootstrap spread if book empty', ]); @file_put_contents( vault_dir() . DIRECTORY_SEPARATOR . 'YEAR_WIPE.txt', "Year wipe epoch $fromN -> $toN at " . gmdate('c') . "\n" . "New admin password in ADMIN_PASSWORD.txt\n" . "Old panel passwords are dead.\n" . "ONLY user NSU balances survived on the ledger.\n" . "Rent siphon: profit/ad piles wiped; liquidity restarts via treasury re-anchor (renters never mint).\n" . "CNY epoch: LORD rent lease ended; operator_addr cleared; next renter gets new seed + new grant.\n" . "Last-year profit stashed for next LORD rent flat-performance term: " . fmt_amt($oldProfit) . " NSU.\n" . "Treasury re-anchor: action=" . ($anchor['action'] ?? '?') . " pop=" . ($anchor['pop'] ?? '?') . " source=" . ($anchor['source'] ?? '?') . " target_fmt=" . (isset($anchor['target']) ? fmt_amt((string)$anchor['target']) : '?') . " after_fmt=" . (isset($anchor['after']) ? fmt_amt((string)$anchor['after']) : '?') . "\n" . "KING faucet re-anchor target = " . NST_KING_FAUCET_START_NSU . " NSU (fixed product scale).\n" . "Rent grant = % remaining faucet + flat(base+last year perf).\n", FILE_APPEND ); } /** * House / empire discovery ads (text). Compartment-local board only. * Recruits sister nosignup.* faces + rare aligned no-signup free tools. * Not a shared ad exchange - each site's pick is independent. */ function house_ad_roster(): array { return [ ['id' => 'house_chat', 'href' => 'https://nosignup.chat/', 'label' => 'nosignup.chat - free peer chat, no account', 'weight' => 3], ['id' => 'house_work', 'href' => 'https://nosignup.work/', 'label' => 'nosignup.work - jobs & resumes, no signup', 'weight' => 2], ['id' => 'house_market', 'href' => 'https://nosignup.market/', 'label' => 'nosignup.market - classifieds, no signup', 'weight' => 2], ['id' => 'house_date', 'href' => 'https://nosignup.date/', 'label' => 'nosignup.date - personals map, no signup', 'weight' => 2], ['id' => 'house_fun', 'href' => 'https://nosignup.fun/', 'label' => 'nosignup.fun - media organism, no signup', 'weight' => 2], ['id' => 'house_info', 'href' => 'https://nosignup.info/', 'label' => 'nosignup.info - free modules, no signup', 'weight' => 2], ['id' => 'house_com', 'href' => 'https://nosignup.com/', 'label' => 'nosignup.com - free forever pinwheel map', 'weight' => 2], ['id' => 'house_org', 'href' => 'https://nosignup.org/', 'label' => 'nosignup.org - free forever MIRROR pinwheel', 'weight' => 2], ['id' => 'house_net', 'href' => 'https://nosignup.net/', 'label' => 'nosignup.net - Morse train, free practice', 'weight' => 2], ['id' => 'house_trade', 'href' => 'https://nosignup.trade/', 'label' => 'nosignup.trade - disposable NSU wallet, no signup', 'weight' => 3], // recruit free/no-signup creatives (email house; not a second economy) ['id' => 'house_recruit', 'href' => 'mailto:buysellfreetrade@proton.me?subject=free%2Fno-signup%20PNG%20house%20ad', 'label' => 'Free/no-signup PNG house ad → buysellfreetrade@proton.me', 'weight' => 2], // aligned free no-signup directory (external; spirit match, not a dependency) ['id' => 'house_fns', 'href' => 'https://freenosignup.com/', 'label' => 'freenosignup.com - free tools, no account (aligned)', 'weight' => 1], ]; } function house_ad_pick(): array { $pool = house_ad_roster(); $total = 0; foreach ($pool as $h) { $total += max(1, (int)($h['weight'] ?? 1)); } $r = random_int(1, max(1, $total)); $acc = 0; $pick = $pool[0]; foreach ($pool as $h) { $acc += max(1, (int)($h['weight'] ?? 1)); if ($r <= $acc) { $pick = $h; break; } } return [ 'id' => (string)$pick['id'], 'active' => true, 'house' => true, 'stake_micros' => '0', 'stake_fmt' => 'house', 'href' => (string)$pick['href'], 'label' => (string)$pick['label'], 'creative_url' => '', 'creative_file' => '', ]; } /** Weighted random active bid that has a creative (site-local board). */ function ad_pick_weighted(): ?array { $e = load_economy(); $pool = []; $totalW = 0; foreach ($e['bids'] ?? [] as $b) { if (empty($b['active'])) continue; $has = !empty($b['creative_url']) || !empty($b['creative_file']); if (!$has) continue; $stake = (string)($b['stake_micros'] ?? '0'); // weight ≈ stake / 0.01 NSU (10000 micros) at least 1 $w = 1; if (function_exists('bcdiv')) { $w = max(1, (int)bcdiv($stake, '10000', 0)); } else { $w = max(1, (int)floor(((float)$stake) / 10000)); } if ($w > 1000000) $w = 1000000; $pool[] = [$b, $w]; $totalW += $w; } // Allotted luck: empty board → house discovery; else rare house roll (empire gossip) if ($totalW < 1 || !$pool) { return house_ad_pick(); } if (random_int(1, 100) <= NST_HOUSE_AD_CHANCE) { return house_ad_pick(); } $r = random_int(1, $totalW); $acc = 0; foreach ($pool as [$b, $w]) { $acc += $w; if ($r <= $acc) return $b; } return $pool[count($pool) - 1][0]; } /** Integer half of a micros string (floor). */ function dec_half(string $a): string { $a = dec_norm($a); if (function_exists('bcdiv')) return dec_norm(bcdiv($a, '2', 0)); $half = ''; $carry = 0; for ($i = 0; $i < strlen($a); $i++) { $n = $carry * 10 + (int)$a[$i]; $half .= (string)intdiv($n, 2); $carry = $n % 2; } return dec_norm($half); } /** Floor-divide decimal string by small positive int (e.g. treasury/1000 = 0.1%). */ function dec_div_small(string $a, int $d): string { $a = dec_norm($a); if ($d <= 1) return $a; if (function_exists('bcdiv')) return dec_norm(bcdiv($a, (string)$d, 0)); $n = ''; $rem = 0; for ($i = 0; $i < strlen($a); $i++) { $cur = $rem * 10 + (int)$a[$i]; $n .= (string)intdiv($cur, $d); $rem = $cur % $d; } return dec_norm($n); } /** * Bootstrap loose-spread order book when there are ZERO open orders. * Temp discovery only: anchor 1.0 NOTE/NSU → BUY @ 0.5, SELL @ 1.5 (micros). * Amount ≈ 0.1% of treasury each side so market makers can tighten. * kind=bootstrap_spread. Does NOT cancel live orders — only posts if book empty. * Discovery quotes only: fill API rejects kind=bootstrap_spread (not free NSU). * * MASTER FAUCET: treasury holds genesis supply (24_000_000 NSU KING start). * That wallet IS the empire faucet wallet for THIS site. Renters must NOT get * treasury.secret / site wallet seed (panel unlock material for owner only). * * @return array{ok:bool,skipped?:bool,reason?:string,err?:string,n?:int,posted?:int,amount_fmt?:string} */ function nst_bootstrap_spread_if_empty(): array { global $TFILE; boot_data(); $chain = read_chain(); $hasG = false; foreach ($chain as $r) { if (($r['type'] ?? '') === 'genesis') { $hasG = true; break; } } if (!$hasG) { return ['ok' => false, 'skipped' => true, 'reason' => 'no_genesis']; } $open = open_orders($chain); if (count($open) > 0) { return ['ok' => true, 'skipped' => true, 'reason' => 'open_orders', 'n' => count($open)]; } if (!is_file($TFILE)) { return ['ok' => false, 'err' => 'no_treasury_secret']; } $sec = trim((string)@file_get_contents($TFILE)); if ($sec === '') { return ['ok' => false, 'err' => 'empty_treasury_secret']; } $addr = addr_from_seed($sec); $bal = balances($chain); $have = (string)($bal[$addr] ?? '0'); $amt = dec_div_small($have, NST_BOOTSTRAP_DENOM); if (dec_cmp($amt, '1') < 0) { return ['ok' => false, 'err' => 'treasury_too_small', 'have_fmt' => fmt_amt($have)]; } // sell side locks NSU; with empty book all liquid is available if (dec_cmp($have, $amt) < 0) { return ['ok' => false, 'err' => 'insufficient', 'have_fmt' => fmt_amt($have)]; } $buyPx = NST_BOOTSTRAP_BUY_PX; $sellPx = NST_BOOTSTRAP_SELL_PX; $anchor = NST_BOOTSTRAP_ANCHOR_PX; $ts = time(); $expiry = $ts + 30 * 86400; $pair = 'NSU/NOTE'; $posted = 0; foreach ([['buy', $buyPx], ['sell', $sellPx]] as [$side, $px]) { $id = hash('sha256', $addr . '|bootstrap|' . $side . '|' . $ts . '|' . $amt . '|' . $px . '|' . bin2hex(random_bytes(4))); $body = implode('|', ['order', '1', $id, $addr, $side, $pair, $amt, $px, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $addr, 'side' => $side, 'pair' => $pair, 'amount' => $amt, 'amount_fmt' => fmt_amt($amt), 'price' => $px, 'price_fmt' => fmt_amt($px), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($sec, $body), 'kind' => 'bootstrap_spread', 'anchor_px' => $anchor, 'note' => 'loose discovery spread around temp anchor 1.0 NOTE/NSU; makers should tighten', ]; if (!append_row($row)) { return ['ok' => false, 'err' => 'disk', 'posted' => $posted]; } $posted++; } append_row([ 'type' => 'bootstrap_spread', 'ver' => 1, 'ts' => $ts, 'buy_px' => $buyPx, 'sell_px' => $sellPx, 'anchor_px' => $anchor, 'amount' => $amt, 'amount_fmt' => fmt_amt($amt), 'treasury_addr' => $addr, 'note' => 'seeded empty book: BUY@0.5 SELL@1.5 around 1.0; ~0.1% treasury each side; P still book-converge', ]); return [ 'ok' => true, 'posted' => $posted, 'amount_fmt' => fmt_amt($amt), 'buy_px_fmt' => fmt_amt($buyPx), 'sell_px_fmt' => fmt_amt($sellPx), 'anchor_px_fmt' => fmt_amt($anchor), ]; } /** * Converged market price P (NOTE micros per 1 NSU). Never a fixed sticker. * both sides → mid; else best bid; else best ask; else null (wait for market). */ function book_mid_micros(array $chain): ?string { $orders = open_orders($chain); $bids = array_values(array_filter($orders, fn($o) => ($o['side'] ?? '') === 'buy')); $asks = array_values(array_filter($orders, fn($o) => ($o['side'] ?? '') === 'sell')); usort($bids, fn($a, $b) => dec_cmp((string)($b['price'] ?? '0'), (string)($a['price'] ?? '0'))); usort($asks, fn($a, $b) => dec_cmp((string)($a['price'] ?? '0'), (string)($b['price'] ?? '0'))); if ($bids && $asks) { $sum = dec_add($bids[0]['price'] ?? '0', $asks[0]['price'] ?? '0'); return dec_half($sum); } if ($bids) return dec_norm((string)($bids[0]['price'] ?? '0')); if ($asks) return dec_norm((string)($asks[0]['price'] ?? '0')); return null; } /** Buyback floor price = P/2 from market convergence. */ function nst_buyback_price_micros(array $chain): ?string { $p = book_mid_micros($chain); if ($p === null || dec_cmp($p, '2') < 0) return null; $half = dec_half($p); return dec_cmp($half, '1') < 0 ? '1' : $half; } /** * Arm treasury BUY order at converged P/2 for nsuAmount micros. * Stable floor: market sets P; we only bid half. No fixed list price. * Fail-open if no mid yet (returns err, does not block gift). */ function nst_arm_buyback_bids(string $nsuAmountMicros, string $reason): array { $nsuAmountMicros = dec_norm($nsuAmountMicros); if (dec_cmp($nsuAmountMicros, '1') < 0) return ['ok' => false, 'err' => 'amount']; $chain = read_chain(); $px = nst_buyback_price_micros($chain); if ($px === null) return ['ok' => false, 'err' => 'no_market_converge', 'note' => 'wait for book; P is never fixed']; $sec = treasury_secret(); $addr = addr_from_seed($sec); $ts = time(); $expiry = $ts + 30 * 86400; $pair = 'NSU/NOTE'; $id = hash('sha256', $addr . '|buyback|' . $ts . '|' . $nsuAmountMicros . '|' . $px . '|' . bin2hex(random_bytes(4))); $body = implode('|', ['order', '1', $id, $addr, 'buy', $pair, $nsuAmountMicros, $px, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $addr, 'side' => 'buy', 'pair' => $pair, 'amount' => $nsuAmountMicros, 'amount_fmt' => fmt_amt($nsuAmountMicros), 'price' => $px, 'price_fmt' => fmt_amt($px), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($sec, $body), 'kind' => 'buyback_floor', 'reason' => substr($reason, 0, 80), 'note' => 'stable floor: BUY at market_P/2 (P from book convergence)', ]; if (!append_row($row)) return ['ok' => false, 'err' => 'disk']; return [ 'ok' => true, 'order' => $row, 'converge_p_fmt' => fmt_amt(book_mid_micros(read_chain()) ?? '0'), 'buyback_px_fmt' => fmt_amt($px), ]; } /** * OPERATOR_GO §3c: treasury BUY at half the sell price, same pair/amount. * Idempotent id = sha256(sellId|treasury_half_bid). Signed with treasury seed. * Quote-side NOTE settles off-chain (settle_ref belief) — same rail as any book BID. * Does not mint; open BID alone does not move balances (conservation held). * * @return array{ok:bool,order?:array,skipped?:bool,reason?:string,err?:string} */ function nst_treasury_half_bid_from_sell(string $sellId, string $pair, string $amount, string $sellPrice, int $expiry, int $ts, array $chain, callable $append, string $tsec, string $taddr): array { $sellId = preg_replace('/[^a-f0-9]/', '', strtolower($sellId)) ?? ''; if (strlen($sellId) !== 64) return ['ok' => false, 'err' => 'bad sell id']; $amount = dec_norm($amount); $sellPrice = dec_norm($sellPrice); if (dec_cmp($amount, '1') < 0 || dec_cmp($sellPrice, '1') < 0) return ['ok' => false, 'err' => 'amount/price']; $halfPx = dec_half($sellPrice); if (dec_cmp($halfPx, '1') < 0) $halfPx = '1'; $pair = strtoupper(trim($pair)); if ($pair === '') $pair = 'NSU/NOTE'; $bidId = hash('sha256', $sellId . '|treasury_half_bid'); foreach ($chain as $r) { if (($r['type'] ?? '') === 'order' && ($r['id'] ?? '') === $bidId) { return ['ok' => true, 'skipped' => true, 'reason' => 'already', 'order' => $r]; } } // Seed/addr MUST be pre-read outside nst_with_chain_lock — never call treasury_secret()/ensure_genesis under LOCK_EX (deadlock). $sec = $tsec; $addr = strtolower($taddr); if ($sec === '' || strlen($addr) !== 64) return ['ok' => false, 'err' => 'no treasury material']; $body = implode('|', ['order', '1', $bidId, $addr, 'buy', $pair, $amount, $halfPx, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $bidId, 'addr' => $addr, 'side' => 'buy', 'pair' => $pair, 'amount' => $amount, 'amount_fmt' => fmt_amt($amount), 'price' => $halfPx, 'price_fmt' => fmt_amt($halfPx), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($sec, $body), 'kind' => 'treasury_half_bid', 'paired_sell_id' => $sellId, 'note' => 'OPERATOR_GO §3c: half-price treasury BID floor paired to treasury SELL', ]; if (!$append($row)) return ['ok' => false, 'err' => 'disk']; return ['ok' => true, 'order' => $row]; } /** * OPERATOR_GO §4 E2a: KING places a small REAL fillable SELL-ask ladder. * Not bootstrap_spread (those stay discovery-only / non-fillable). * Each SELL is an ordinary book order signed by treasury → C2 half-BID arms. * Deterministic ids (sha256 taddr|treasury_ladder_v1|price) → re-call is idempotent. * Seed/addr MUST be pre-read outside nst_with_chain_lock (same deadlock law as C2). * * @return array{ok:bool,posted?:int,skipped?:int,orders?:array,half_bids?:array,err?:string,amount_fmt?:string} */ function nst_treasury_ask_ladder(string $tsec, string $taddr, ?string $amountMicros = null): array { $tsec = (string)$tsec; $taddr = strtolower(trim($taddr)); if ($tsec === '' || strlen($taddr) !== 64) { return ['ok' => false, 'err' => 'no treasury material']; } $amt = $amountMicros !== null && $amountMicros !== '' ? dec_norm($amountMicros) : parse_amt(NST_LADDER_AMT_NSU); if ($amt === null || dec_cmp($amt, '1') < 0) { return ['ok' => false, 'err' => 'amount']; } $amtMax = parse_amt(NST_LADDER_AMT_MAX_NSU); if ($amtMax !== null && dec_cmp($amt, $amtMax) > 0) { return ['ok' => false, 'err' => 'amount exceeds ladder cap', 'max_fmt' => fmt_amt($amtMax)]; } $prices = [NST_LADDER_PX_LO, NST_LADDER_PX_MID, NST_LADDER_PX_HI]; $pair = 'NSU/NOTE'; $ts = time(); $expiry = $ts + 30 * 86400; $out = nst_with_chain_lock(function (array $chain, callable $append) use ( $tsec, $taddr, $amt, $prices, $pair, $ts, $expiry ) { $bal = balances($chain); $have = (string)($bal[$taddr] ?? '0'); $av = available_nsu($bal, $chain, $taddr); $need = '0'; foreach ($prices as $px) { $need = dec_add($need, $amt); } if (dec_cmp($av, $need) < 0) { return [ 'ok' => false, 'err' => 'insufficient available NSU', 'available_fmt' => fmt_amt($av), 'need_fmt' => fmt_amt($need), 'treasury_fmt' => fmt_amt($have), ]; } $nOpen = 0; foreach (open_orders($chain) as $o) { if (($o['addr'] ?? '') === $taddr) { $nOpen++; } } // Each new sell + half-BID needs 2 slots; leave headroom under 20. $slotsNeeded = count($prices) * 2; if ($nOpen + $slotsNeeded > 20) { return ['ok' => false, 'err' => 'too many open orders', 'open' => $nOpen]; } $posted = 0; $skipped = 0; $orders = []; $halfBids = []; foreach ($prices as $px) { $px = dec_norm($px); if (dec_cmp($px, '1') < 0) { continue; } $id = hash('sha256', $taddr . '|treasury_ladder_v1|' . $px); $exists = false; foreach ($chain as $r) { if (($r['type'] ?? '') === 'order' && ($r['id'] ?? '') === $id) { $exists = true; break; } } if ($exists) { $skipped++; continue; } $body = implode('|', ['order', '1', $id, $taddr, 'sell', $pair, $amt, $px, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $taddr, 'side' => 'sell', 'pair' => $pair, 'amount' => $amt, 'amount_fmt' => fmt_amt($amt), 'price' => $px, 'price_fmt' => fmt_amt($px), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($tsec, $body), 'kind' => 'treasury_ladder', 'note' => 'OPERATOR_GO §4: KING real fillable SELL ladder (buy path); not bootstrap discovery', ]; if (!$append($row)) { return ['ok' => false, 'err' => 'disk', 'posted' => $posted, 'orders' => $orders]; } $hb = nst_treasury_half_bid_from_sell( $id, $pair, $amt, $px, $expiry, $ts, $chain, $append, $tsec, $taddr ); if (empty($hb['ok'])) { return [ 'ok' => false, 'err' => 'treasury half-bid failed after ladder sell', 'detail' => $hb['err'] ?? 'unknown', 'posted' => $posted, 'order' => $row, ]; } $posted++; $orders[] = $row; if (!empty($hb['order'])) { $halfBids[] = $hb['order']; } // Keep open-count honest for later rungs in same lock. $nOpen += 2; } return [ 'ok' => true, 'posted' => $posted, 'skipped' => $skipped, 'orders' => $orders, 'half_bids' => $halfBids, 'amount_fmt' => fmt_amt($amt), 'prices_fmt' => array_map('fmt_amt', $prices), 'note' => 'Real fillable treasury asks on book; visitors TAKE ORDER / ?api=fill. NOTE settles off-chain (settle_ref).', ]; }); if ($out === null) { return ['ok' => false, 'err' => 'disk']; } return $out; } /** * Donate → NSU gift (mid×k or direct), then 50% profit pile / 50% ad-bid stake. * Faucet free gas does NOT use this path. */ function economy_credit_donate(string $toAddr, string $nsuMicros, string $txid, string $asset, string $extAmt): array { $nsuMicros = dec_norm($nsuMicros); if (dec_cmp($nsuMicros, '1') < 0) return ['ok' => false, 'err' => 'gift too small']; $sec = treasury_secret(); $from = addr_from_seed($sec); // half ad stake / half profit pile; then arm BUYBACK at market P/2 for the ad half // (stable floor: P converges from book — never a fixed sticker price) $half = dec_half($nsuMicros); $bidPart = $half; $profitPart = dec_sub($nsuMicros, $bidPart); $ts = time(); $nonce = bin2hex(random_bytes(8)); $memo = 'donate:gift:' . substr($txid !== '' ? $txid : 'honor', 0, 40); $tbody = implode('|', ['transfer', '1', $from, $toAddr, $nsuMicros, $nonce, (string)$ts, $memo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $toAddr, 'amount' => $nsuMicros, 'amount_fmt' => fmt_amt($nsuMicros), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => mac_sign($sec, $tbody), 'kind' => 'donate_gift', 'txid' => substr($txid, 0, 128), 'asset' => substr($asset, 0, 16), ]; $splitRow = [ 'type' => 'economy_split', 'ver' => 1, 'ts' => $ts, 'gift_fmt' => fmt_amt($nsuMicros), 'profit_fmt' => fmt_amt($profitPart), 'ad_bid_fmt' => fmt_amt($bidPart), 'txid' => substr($txid, 0, 128), 'note' => '50% profit pile / 50% site ad bid (donate path); buyback arms at market P/2', ]; $creditOut = nst_with_chain_lock(function (array $chain, callable $append) use ( $from, $toAddr, $nsuMicros, $nonce, $row, $splitRow, $bidPart, $profitPart, $ts, $txid, $asset, $extAmt ) { $bal = balances($chain); $have = $bal[$from] ?? '0'; if (dec_cmp($have, $nsuMicros) < 0) { return ['ok' => false, 'err' => 'treasury insufficient', 'have_fmt' => fmt_amt($have)]; } foreach ($chain as $r) { if (($r['type'] ?? '') === 'transfer' && ($r['from'] ?? '') === $from && ($r['nonce'] ?? '') === $nonce) { return ['ok' => false, 'err' => 'nonce replay']; } } if (!$append($row)) return ['ok' => false, 'err' => 'disk']; $e = load_economy(); $e['profit_micros'] = dec_add((string)($e['profit_micros'] ?? '0'), $profitPart); $e['ad_bid_micros'] = dec_add((string)($e['ad_bid_micros'] ?? '0'), $bidPart); $bid = [ 'id' => hash('sha256', $toAddr . '|' . $ts . '|' . $bidPart . '|' . $nonce), 'stake_micros' => $bidPart, 'stake_fmt' => fmt_amt($bidPart), 'ts' => $ts, 'from_donate' => true, 'txid' => substr($txid, 0, 128), 'active' => true, ]; if (!isset($e['bids']) || !is_array($e['bids'])) $e['bids'] = []; $e['bids'][] = $bid; if (count($e['bids']) > 200) $e['bids'] = array_slice($e['bids'], -200); if (!isset($e['donations']) || !is_array($e['donations'])) $e['donations'] = []; $e['donations'][] = [ 'ts' => $ts, 'to' => $toAddr, 'gift_micros' => $nsuMicros, 'gift_fmt' => fmt_amt($nsuMicros), 'profit' => $profitPart, 'ad_bid' => $bidPart, 'txid' => substr($txid, 0, 128), 'asset' => substr($asset, 0, 16), 'ext' => substr($extAmt, 0, 40), ]; if (count($e['donations']) > 200) $e['donations'] = array_slice($e['donations'], -200); save_economy($e); if (!$append($splitRow)) { return ['ok' => false, 'err' => 'disk']; } return [ 'ok' => true, 'tx' => $row, 'gift_fmt' => fmt_amt($nsuMicros), 'profit_fmt' => fmt_amt($profitPart), 'ad_bid_fmt' => fmt_amt($bidPart), 'economy' => [ 'profit_fmt' => fmt_amt((string)$e['profit_micros']), 'ad_bid_fmt' => fmt_amt((string)$e['ad_bid_micros']), ], ]; }); if ($creditOut === null) return ['ok' => false, 'err' => 'disk']; if (empty($creditOut['ok'])) return $creditOut; // Lock floor: treasury BUY at converged P/2 for the ad-half quantity (fail-open if no book yet) $bb = nst_arm_buyback_bids($bidPart, 'donate_buyback'); if (!empty($bb['ok'])) { append_row([ 'type' => 'buyback_arm', 'ver' => 1, 'ts' => time(), 'amount_fmt' => fmt_amt($bidPart), 'buyback_px_fmt' => $bb['buyback_px_fmt'] ?? null, 'order_id' => $bb['order']['id'] ?? null, 'note' => 'market-converged P/2 floor bid (not fixed list price)', ]); } return [ 'ok' => true, 'tx' => $creditOut['tx'] ?? $row, 'gift_fmt' => $creditOut['gift_fmt'] ?? fmt_amt($nsuMicros), 'profit_fmt' => $creditOut['profit_fmt'] ?? fmt_amt($profitPart), 'ad_bid_fmt' => $creditOut['ad_bid_fmt'] ?? fmt_amt($bidPart), 'buyback' => $bb, 'price_policy' => 'market_converge_P_then_bid_P_over_2', 'economy' => $creditOut['economy'] ?? null, ]; } /** Convert external amount + book mid → NSU micros. mid = quote per 1 NSU (in micros of quote). */ function donate_nsu_from_external(string $extHuman, ?string $midMicros, string $k): ?string { $ext = parse_amt($extHuman); if ($ext === null || dec_cmp($ext, '1') < 0) return null; // If mid present: nsu = ext / mid * k (all integer micros) // ext and mid same decimal basis (6). nsu_micros = ext * 1e6 / mid * k ≈ ext/mid when k=1 // Actually both are already micros: nsu = floor(ext * k / mid) when mid is price in quote-micros per NSU unit? // price_fmt "2" means 2 NOTE per NSU → price micros = 2000000. amount 1 NSU = 1000000 micros. // gift NSU micros = ext_micros * 1e6 / mid_micros ... use: nsu = ext / mid * 10^0 in unit space. // unit: nsu_units = ext_units / mid_units * k → micros: nsu_m = ext_m * k / mid_m * 10^6 / 10^6 = ext*k/mid if ($midMicros !== null && dec_cmp($midMicros, '1') > 0) { if (function_exists('bcmul') && function_exists('bcdiv')) { $num = bcmul($ext, $k, 0); return dec_norm(bcdiv($num, $midMicros, 0)); } // fallback float for small values only $n = (float)$ext * (float)$k / (float)$midMicros; if ($n < 1) return null; return dec_norm((string)(int)floor($n)); } // no mid: nsu = ext * k if (function_exists('bcmul')) return dec_norm(bcmul($ext, $k, 0)); return dec_mul_small($ext, max(1, (int)$k)); } function ensure_meta_epochs(): array { $m = load_meta(); $now = time(); if (empty($m['epoch_start'])) { $m['epoch_start'] = $now; $m['epoch_n'] = 1; } // advance yearly reset windows until current; roll economy stakes forward while (($m['epoch_start'] + NST_RESET_SECS) <= $now) { $fromN = (int)($m['epoch_n'] ?? 1); $m['epoch_start'] = (int)$m['epoch_start'] + NST_RESET_SECS; $m['epoch_n'] = $fromN + 1; economy_on_epoch_roll($fromN, (int)$m['epoch_n']); } if (empty($m['created'])) $m['created'] = $now; $m['version'] = NST_VERSION; save_meta($m); return $m; } function epoch_info(): array { $m = ensure_meta_epochs(); $start = (int)$m['epoch_start']; $end = $start + NST_RESET_SECS; $winEnd = $start + NST_EARLY_WINDOW_SECS; $now = time(); return [ 'epoch_n' => (int)($m['epoch_n'] ?? 1), 'epoch_start' => $start, 'epoch_end' => $end, 'early_window_end' => $winEnd, 'in_early_window' => $now >= $start && $now < $winEnd, 'secs_to_window_end' => max(0, $winEnd - $now), 'secs_to_reset' => max(0, $end - $now), ]; } /** Rentable crop ids (empire ten). Quotes only — settle/bind is a later residual. */ function nst_rent_site_ids(): array { return ['com', 'org', 'net', 'trade', 'chat', 'work', 'market', 'date', 'fun', 'info']; } /** Path to durable rent quote map (data/rent_quotes.json). */ function nst_rent_quotes_path(): string { global $RENT_QUOTES_FILE; return $RENT_QUOTES_FILE; } /** * Exclusive RMW on rent_quotes.json via flock. * Callback: fn(array $state): array — return new full state to write (or same). * State shape: ['quotes' => [quote_id => row, ...], 'updated' => ts] * @return mixed callback result, or null on lock/open failure */ function nst_with_rent_quotes_lock(callable $fn) { boot_data(); $path = nst_rent_quotes_path(); return nst_with_file_lock($path, function ($fh) use ($fn) { rewind($fh); $raw = stream_get_contents($fh); $state = ['quotes' => [], 'updated' => 0]; if (is_string($raw) && trim($raw) !== '') { $j = json_decode($raw, true); if (is_array($j)) { $state = array_merge($state, $j); if (!isset($state['quotes']) || !is_array($state['quotes'])) { $state['quotes'] = []; } } } $ret = $fn($state); // If callback returned a state-shaped array with quotes, persist it. if (is_array($ret) && isset($ret['quotes']) && is_array($ret['quotes'])) { $ret['updated'] = time(); $toWrite = $ret; unset($toWrite['_last']); // never persist helper side-channel $blob = j($toWrite); ftruncate($fh, 0); rewind($fh); if (fwrite($fh, $blob) === false) { return null; } fflush($fh); } return $ret; }); } /** Drop expired open quotes; keep paid ~7d for first-paid-wins / audit; thrift cap. */ function nst_rent_quotes_prune(array $state, int $now): array { $quotes = $state['quotes'] ?? []; if (!is_array($quotes)) { $quotes = []; } $kept = []; $paidKeepSecs = 7 * 86400; foreach ($quotes as $id => $row) { if (!is_array($row)) { continue; } $st = (string)($row['status'] ?? 'open'); $qid = (string)($row['quote_id'] ?? $id); if ($qid === '') { continue; } if ($st === 'paid') { $paidTs = (int)($row['paid_ts'] ?? 0); if ($paidTs > 0 && ($now - $paidTs) > $paidKeepSecs) { continue; } $kept[$qid] = $row; continue; } if ($st !== 'open') { continue; } $exp = (int)($row['expires'] ?? 0); if ($exp > 0 && $exp < $now) { continue; } $kept[$qid] = $row; } // Cap: prefer paid + newest open; hard ceiling 250 rows if (count($kept) > 250) { uasort($kept, static function ($a, $b) { $sa = (string)($a['status'] ?? ''); $sb = (string)($b['status'] ?? ''); if ($sa === 'paid' && $sb !== 'paid') { return -1; } if ($sb === 'paid' && $sa !== 'paid') { return 1; } return ((int)($b['created'] ?? 0)) <=> ((int)($a['created'] ?? 0)); }); $kept = array_slice($kept, 0, 250, true); } $state['quotes'] = $kept; return $state; } /** * Persist one open quote. Returns stored row or null. * @param array $row must include quote_id, site, pay_to, amount, memo, expires, … */ function nst_rent_quote_store(array $row): ?array { $qid = (string)($row['quote_id'] ?? ''); if ($qid === '' || !preg_match('/^[a-f0-9]{16,64}$/', $qid)) { return null; } $now = time(); $out = nst_with_rent_quotes_lock(function (array $state) use ($row, $qid, $now) { $state = nst_rent_quotes_prune($state, $now); $row['status'] = 'open'; $row['created'] = (int)($row['created'] ?? $now); $state['quotes'][$qid] = $row; // Return full state so lock helper writes; also stash row for return via side channel $state['_last'] = $row; return $state; }); if (!is_array($out) || empty($out['_last']) || !is_array($out['_last'])) { return null; } return $out['_last']; } /** Lookup quote by id (open non-expired, or paid). For settle residual / verify. */ function nst_rent_quote_get(string $quoteId): ?array { $quoteId = strtolower(preg_replace('/[^a-f0-9]/', '', $quoteId) ?? ''); if ($quoteId === '') { return null; } $now = time(); $found = null; nst_with_rent_quotes_lock(function (array $state) use ($quoteId, $now, &$found) { $state = nst_rent_quotes_prune($state, $now); $row = $state['quotes'][$quoteId] ?? null; if (is_array($row)) { $st = (string)($row['status'] ?? ''); if ($st === 'paid') { $found = $row; } elseif ($st === 'open') { $exp = (int)($row['expires'] ?? 0); if ($exp <= 0 || $exp >= $now) { $found = $row; } } } return $state; // write pruned state }); return $found; } /** Parse memo rent:{site}:{quote_id}. */ function nst_rent_parse_memo(string $memo): ?array { $memo = trim($memo); if (!preg_match('/^rent:([a-z]+):([a-f0-9]{16,64})$/i', $memo, $m)) { return null; } return ['site' => strtolower($m[1]), 'quote_id' => strtolower($m[2])]; } /** Settle failure reasons that should auto-refund the payer (P3d). */ function nst_rent_refundable_reason(string $reason): bool { return in_array($reason, [ 'already_paid', 'amount_mismatch', 'quote_missing', 'quote_expired', 'quote_not_open', 'site_mismatch', 'pay_to_mismatch', 'already_leased', 'bad_site', ], true); } /** * P3d: refund a failed rent pay from treasury → original payer (conserved). * Idempotent per inbound transfer nonce (memo rent:refund:{qid}:{nonce}). * @param array $failedTx original inbound transfer to treasury * @param array $settle result of nst_rent_try_settle_transfer * @return array{ok:bool,refunded?:bool,reason?:string,tx?:array,err?:string} */ function nst_rent_auto_refund(array $failedTx, array $settle): array { if (!empty($settle['settled'])) { return ['ok' => true, 'refunded' => false, 'reason' => 'settled_no_refund']; } $why = (string)($settle['reason'] ?? ''); if (!nst_rent_refundable_reason($why)) { return ['ok' => true, 'refunded' => false, 'reason' => 'not_refundable:' . $why]; } if (function_exists('nst_conservation_is_frozen') && nst_conservation_is_frozen()) { return ['ok' => false, 'refunded' => false, 'err' => 'writes frozen']; } $payer = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($failedTx['from'] ?? '')) ?? ''); $amount = dec_norm((string)($failedTx['amount'] ?? '0')); $inNonce = substr((string)($failedTx['nonce'] ?? ''), 0, 64); $parsed = nst_rent_parse_memo((string)($failedTx['memo'] ?? '')); $qid = $parsed['quote_id'] ?? 'unknown'; if (strlen($payer) !== 64 || !dec_ok($amount) || dec_cmp($amount, '1') < 0 || $inNonce === '') { return ['ok' => false, 'refunded' => false, 'err' => 'bad_failed_tx']; } $sec = treasury_secret(); $from = strtolower(addr_from_seed($sec)); $taddr = strtolower(treasury_addr()); if ($from !== $taddr) { return ['ok' => false, 'refunded' => false, 'err' => 'treasury_mismatch']; } // Idempotent refund nonce derived from inbound nonce (stable). $refundNonce = substr(hash('sha256', 'rent-refund-v1|' . $inNonce), 0, 32); $refundMemo = 'rent:refund:' . $qid . ':' . $inNonce; $ts = time(); $body = implode('|', ['transfer', '1', $from, $payer, $amount, $refundNonce, (string)$ts, $refundMemo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $payer, 'amount' => $amount, 'amount_fmt' => fmt_amt($amount), 'nonce' => $refundNonce, 'ts' => $ts, 'memo' => $refundMemo, 'sig' => mac_sign($sec, $body), 'sig_mode' => 'server_mac', 'kind' => 'rent_refund', 'rent_refund_of_nonce' => $inNonce, 'rent_refund_reason' => $why, 'quote_id' => $qid, ]; $out = nst_with_chain_lock(function (array $chain, callable $append) use ($from, $payer, $amount, $refundNonce, $inNonce, $row) { foreach ($chain as $r) { if (($r['type'] ?? '') !== 'transfer') { continue; } // already refunded this inbound pay if ((string)($r['rent_refund_of_nonce'] ?? '') === $inNonce) { return ['ok' => true, 'refunded' => false, 'reason' => 'already_refunded', 'tx' => $r]; } if ((string)($r['nonce'] ?? '') === $refundNonce && strtolower((string)($r['from'] ?? '')) === $from) { return ['ok' => true, 'refunded' => false, 'reason' => 'already_refunded', 'tx' => $r]; } } $bal = balances($chain); $have = $bal[$from] ?? '0'; if (dec_cmp($have, $amount) < 0) { return [ 'ok' => false, 'refunded' => false, 'err' => 'treasury insufficient for refund', 'have_fmt' => fmt_amt($have), 'need_fmt' => fmt_amt($amount), ]; } if (!$append($row)) { return ['ok' => false, 'refunded' => false, 'err' => 'disk']; } return [ 'ok' => true, 'refunded' => true, 'tx' => $row, 'treasury_bal_fmt' => fmt_amt(dec_sub($have, $amount)), 'payer_credit_fmt' => fmt_amt($amount), ]; }); if ($out === null) { return ['ok' => false, 'refunded' => false, 'err' => 'disk']; } $out['reason'] = $why; $out['law'] = 'auto-refund failed rent pay · conserved · idempotent per inbound nonce'; return $out; } /** * P3c: try to settle a rent payment from an on-chain transfer row. * first-paid-wins · memo+amount+pay_to match · bind operator_addr = payer (no server seed). * P3d: caller may auto-refund when reason is refundable. * @param array $tx transfer row (from,to,amount,memo,nonce,ts,…) * @return array{settled:bool,reason?:string,site?:string,payer?:string,quote_id?:string,lease?:array} */ function nst_rent_try_settle_transfer(array $tx): array { $parsed = nst_rent_parse_memo((string)($tx['memo'] ?? '')); if ($parsed === null) { return ['settled' => false, 'reason' => 'not_rent_memo']; } $site = $parsed['site']; $qid = $parsed['quote_id']; if (!in_array($site, nst_rent_site_ids(), true)) { return ['settled' => false, 'reason' => 'bad_site']; } $to = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($tx['to'] ?? '')) ?? ''); $from = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($tx['from'] ?? '')) ?? ''); $amount = dec_norm((string)($tx['amount'] ?? '0')); if (strlen($to) !== 64 || strlen($from) !== 64 || !dec_ok($amount) || dec_cmp($amount, '1') < 0) { return ['settled' => false, 'reason' => 'bad_tx_fields']; } $taddr = strtolower(treasury_addr()); if ($to !== $taddr) { return ['settled' => false, 'reason' => 'not_to_treasury']; } $result = ['settled' => false, 'reason' => 'lock_failed']; $lockRet = nst_with_rent_quotes_lock(function (array $state) use ($site, $qid, $from, $to, $amount, $taddr, $tx, &$result) { $now = time(); $state = nst_rent_quotes_prune($state, $now); $row = $state['quotes'][$qid] ?? null; if (!is_array($row)) { $result = ['settled' => false, 'reason' => 'quote_missing']; return $state; } $st = (string)($row['status'] ?? 'open'); if ($st === 'paid') { $result = [ 'settled' => false, 'reason' => 'already_paid', 'winner' => (string)($row['payer'] ?? ''), 'quote_id' => $qid, ]; return $state; } if ($st !== 'open') { $result = ['settled' => false, 'reason' => 'quote_not_open', 'status' => $st]; return $state; } $exp = (int)($row['expires'] ?? 0); if ($exp > 0 && $exp < $now) { $result = ['settled' => false, 'reason' => 'quote_expired']; return $state; } if (strtolower((string)($row['site'] ?? '')) !== $site) { $result = ['settled' => false, 'reason' => 'site_mismatch']; return $state; } if (strtolower((string)($row['pay_to'] ?? '')) !== $taddr) { $result = ['settled' => false, 'reason' => 'pay_to_mismatch']; return $state; } if (dec_cmp(dec_norm((string)($row['amount'] ?? '0')), $amount) !== 0) { $result = ['settled' => false, 'reason' => 'amount_mismatch', 'expected' => (string)($row['amount'] ?? '0'), 'got' => $amount]; return $state; } // trade crop: refuse if already leased to someone else $e = load_economy(); if ($site === 'trade') { $opHave = strtolower(trim((string)($e['operator_addr'] ?? ''))); $rented = !empty($e['rented']) && $opHave !== '' && $opHave !== $taddr; if ($rented) { $result = ['settled' => false, 'reason' => 'already_leased', 'operator_addr' => $opHave]; return $state; } } $row['status'] = 'paid'; $row['payer'] = $from; $row['paid_ts'] = $now; $row['paid_tx'] = [ 'from' => $from, 'to' => $to, 'amount' => $amount, 'nonce' => (string)($tx['nonce'] ?? ''), 'ts' => (int)($tx['ts'] ?? $now), 'memo' => (string)($tx['memo'] ?? ''), ]; $state['quotes'][$qid] = $row; $lease = [ 'operator_addr' => $from, 'rented_at' => $now, 'quote_id' => $qid, 'site' => $site, 'epoch_n' => (int)($row['epoch_n'] ?? 0), 'amount' => $amount, 'amount_fmt' => fmt_amt($amount), ]; if ($site === 'trade') { $e['rented'] = true; $e['operator_addr'] = $from; $e['rented_at'] = $now; $e['rent_quote_id'] = $qid; $e['rent_payer'] = $from; } if (!isset($e['leases']) || !is_array($e['leases'])) { $e['leases'] = []; } $e['leases'][$site] = $lease; save_economy($e); if ($site === 'trade') { global $OP_ADDR_FILE; boot_data(); file_put_contents($OP_ADDR_FILE, $from . "\n", LOCK_EX); @chmod($OP_ADDR_FILE, 0644); } $result = [ 'settled' => true, 'site' => $site, 'payer' => $from, 'quote_id' => $qid, 'lease' => $lease, 'law' => 'first-paid-wins · operator_addr = payer key · no server LORD seed', ]; return $state; }); if ($lockRet === null) { return ['settled' => false, 'reason' => 'lock_failed']; } return $result; } /** * Scan chain for a transfer that pays a given open quote (or any open quote if id empty). * Idempotent: already-paid quotes report already_paid. */ function nst_rent_settle_scan(?string $quoteId = null): array { $quoteId = $quoteId !== null ? strtolower(preg_replace('/[^a-f0-9]/', '', $quoteId) ?? '') : ''; $chain = read_chain(); $taddr = strtolower(treasury_addr()); $attempts = []; $settled = null; foreach (array_reverse($chain) as $r) { if (($r['type'] ?? '') !== 'transfer') { continue; } if (strtolower((string)($r['to'] ?? '')) !== $taddr) { continue; } $parsed = nst_rent_parse_memo((string)($r['memo'] ?? '')); if ($parsed === null) { continue; } if ($quoteId !== '' && $parsed['quote_id'] !== $quoteId) { continue; } $out = nst_rent_try_settle_transfer($r); $refund = null; if (empty($out['settled']) && nst_rent_refundable_reason((string)($out['reason'] ?? ''))) { $refund = nst_rent_auto_refund($r, $out); } // Fail-open server_notice after money/rent residual commits (scan path). try { if (!empty($out['settled'])) { nst_inbox_notify_rent_settle($out); } if (is_array($refund) && !empty($refund['refunded'])) { nst_inbox_notify_rent_refund($refund); } } catch (Throwable $e) { // never abort settle/refund residual } $attempts[] = [ 'quote_id' => $parsed['quote_id'], 'from' => (string)($r['from'] ?? ''), 'result' => $out, 'refund' => $refund, ]; if (!empty($out['settled'])) { $settled = $out; break; } if (count($attempts) >= 40) { break; } } return [ 'ok' => true, 'settled' => $settled !== null, 'lease' => $settled['lease'] ?? null, 'settle' => $settled, 'attempts' => $attempts, 'law' => 'scan · settle first-paid-wins · auto-refund failed rent pays (P3d)', ]; } /** * Public rent quote (P3a+P3b). No chain write, no seed, no operator bind. * amount = NST_RENT_NSU_PER_DAY * days_left (days_left = ceil(secs_to_reset/86400), min 1). * P3b: persists open quote under flock for later settle match. * @return array JSON-ready */ function nst_rent_quote_public(string $site, string $treasuryAddr): array { $site = strtolower(trim($site)); if ($site === '' || $site === 'this' || $site === 'me') { $site = 'trade'; } $allowed = nst_rent_site_ids(); if (!in_array($site, $allowed, true)) { return [ 'ok' => false, 'http' => 400, 'err' => 'bad site', 'sites' => $allowed, ]; } $ep = epoch_info(); $secs = (int)($ep['secs_to_reset'] ?? 0); $daysLeft = (int)max(1, (int)ceil($secs / 86400)); $amountNsu = (int)NST_RENT_NSU_PER_DAY * $daysLeft; $amountMicros = parse_amt((string)$amountNsu); if ($amountMicros === null || dec_cmp($amountMicros, '1') < 0) { return ['ok' => false, 'http' => 500, 'err' => 'rent amount invalid']; } // This crop's economy only tracks trade lease today; other sites quote as free until multi-lease store. $e = load_economy(); $op = strtolower(trim((string)($e['operator_addr'] ?? ''))); $t = strtolower($treasuryAddr); $rentedHere = !empty($e['rented']) && $op !== '' && $op !== $t; if ($site === 'trade' && $rentedHere) { return [ 'ok' => true, 'quoted' => false, 'site' => $site, 'leased' => true, 'leased_until' => (int)($ep['epoch_end'] ?? 0), 'operator_addr' => $op, 'rented_at' => (int)($e['rented_at'] ?? 0), 'err' => 'already leased', 'law' => 'Occupied crop: no new quote until epoch wipe (or future multi-lease free).', ]; } $now = time(); $ttl = (int)NST_RENT_QUOTE_TTL_SECS; if ($ttl < 60) { $ttl = 1800; } $quoteId = bin2hex(random_bytes(12)); $memo = 'rent:' . $site . ':' . $quoteId; $expires = $now + $ttl; $storeRow = [ 'quote_id' => $quoteId, 'site' => $site, 'pay_to' => $treasuryAddr, 'amount' => $amountMicros, 'amount_nsu' => (string)$amountNsu, 'nsu_per_day' => (int)NST_RENT_NSU_PER_DAY, 'days_left' => $daysLeft, 'memo' => $memo, 'expires' => $expires, 'created' => $now, 'epoch_n' => (int)($ep['epoch_n'] ?? 0), 'epoch_end' => (int)($ep['epoch_end'] ?? 0), 'status' => 'open', ]; $stored = nst_rent_quote_store($storeRow); if ($stored === null) { return [ 'ok' => false, 'http' => 500, 'err' => 'quote store failed', 'durable' => false, 'settle' => false, ]; } return [ 'ok' => true, 'quoted' => true, 'site' => $site, 'leased' => false, 'quote_id' => $quoteId, 'pay_to' => $treasuryAddr, 'amount' => $amountMicros, 'amount_fmt' => fmt_amt($amountMicros), 'amount_nsu' => (string)$amountNsu, 'nsu_per_day' => (int)NST_RENT_NSU_PER_DAY, 'days_left' => $daysLeft, 'memo' => $memo, 'expires' => $expires, 'expires_in_secs' => $ttl, 'epoch_n' => (int)($ep['epoch_n'] ?? 0), 'epoch_end' => (int)($ep['epoch_end'] ?? 0), 'durable' => true, 'settle' => false, 'law' => 'Quote durable on this host (P3b data/rent_quotes.json). Pay-to-treasury settle + payer-key bind not shipped.', ]; } function load_mods(): array { global $MODS_FILE; if (!is_file($MODS_FILE)) return []; $j = json_decode((string)file_get_contents($MODS_FILE), true); return is_array($j) ? $j : []; } function save_mods(array $mods): void { global $MODS_FILE; file_put_contents($MODS_FILE, j($mods), LOCK_EX); } function grant_early_modifier(string $addr): array { $ep = epoch_info(); $mods = load_mods(); if (!empty($mods[$addr]['permanent'])) { return ['granted' => false, 'reason' => 'already', 'mod' => $mods[$addr]]; } if (!$ep['in_early_window']) { return ['granted' => false, 'reason' => 'outside_window', 'epoch' => $ep]; } $now = time(); $mods[$addr] = [ 'permanent' => true, 'daily_micros' => NST_MOD_DAILY_MICROS, 'granted_ts' => $now, 'granted_epoch' => $ep['epoch_n'], 'last_accrual_ts' => $now, // start accruing from grant moment ]; save_mods($mods); // public record (no secrets) append_row([ 'type' => 'modifier_grant', 'ver' => 1, 'addr' => $addr, 'daily_fmt' => '0.1', 'ts' => $now, 'epoch_n' => $ep['epoch_n'], 'note' => 'permanent early-access modifier (30d window after yearly reset)', ]); return ['granted' => true, 'mod' => $mods[$addr], 'epoch' => $ep]; } /** Lazy catch-up: pay floor(days elapsed)*0.1 from treasury if permanent modifier */ function lazy_accrue(string $addr): array { $modsPeek = load_mods(); if (empty($modsPeek[$addr]['permanent'])) { return ['ok' => true, 'paid_fmt' => '0', 'reason' => 'no_modifier']; } $nowPeek = time(); $lastPeek = (int)($modsPeek[$addr]['last_accrual_ts'] ?? $nowPeek); if ($lastPeek > $nowPeek) $lastPeek = $nowPeek; if (intdiv($nowPeek - $lastPeek, 86400) < 1) { return ['ok' => true, 'paid_fmt' => '0', 'reason' => 'too_soon', 'next_in' => 86400 - (($nowPeek - $lastPeek) % 86400)]; } $sec = treasury_secret(); $from = addr_from_seed($sec); // Chain RMW + re-load mods under lock so concurrent unlocks cannot double-pay days. $out = nst_with_chain_lock(function (array $chain, callable $append) use ($addr, $from, $sec) { $mods = load_mods(); if (empty($mods[$addr]['permanent'])) { return ['ok' => true, 'paid_fmt' => '0', 'reason' => 'no_modifier']; } $now = time(); $last = (int)($mods[$addr]['last_accrual_ts'] ?? $now); if ($last > $now) $last = $now; $days = intdiv($now - $last, 86400); if ($days < 1) { return ['ok' => true, 'paid_fmt' => '0', 'reason' => 'too_soon', 'next_in' => 86400 - (($now - $last) % 86400)]; } $pay = dec_mul_small(NST_MOD_DAILY_MICROS, $days); $bal = balances($chain); $have = $bal[$from] ?? '0'; if (dec_cmp($have, $pay) < 0) { // whole days treasury can afford only (lazy ketchup, no partial-day mint) $daysCan = 0; $payCan = '0'; for ($d = 1; $d <= $days; $d++) { $try = dec_mul_small(NST_MOD_DAILY_MICROS, $d); if (dec_cmp($try, $have) > 0) break; $daysCan = $d; $payCan = $try; } if ($daysCan < 1) { return ['ok' => false, 'err' => 'treasury empty', 'paid_fmt' => '0']; } $days = $daysCan; $pay = $payCan; } $ts = $now; $nonce = bin2hex(random_bytes(8)); $memo = 'modifier:daily:' . $days . 'd'; $tbody = implode('|', ['transfer', '1', $from, $addr, $pay, $nonce, (string)$ts, $memo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $addr, 'amount' => $pay, 'amount_fmt' => fmt_amt($pay), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => mac_sign($sec, $tbody), 'kind' => 'modifier_accrual', ]; if (!$append($row)) { return ['ok' => false, 'err' => 'disk']; } // advance last_accrual by whole days only (while still holding chain lock) $mods[$addr]['last_accrual_ts'] = $last + ($days * 86400); save_mods($mods); return ['ok' => true, 'paid_fmt' => fmt_amt($pay), 'days' => $days, 'tx' => $row]; }); if ($out === null) { return ['ok' => false, 'err' => 'disk', 'paid_fmt' => '0']; } return $out; } function vault_dir(): string { global $VAULT, $ROOT; $candidates = [ $VAULT, $ROOT . DIRECTORY_SEPARATOR . 'vault', // fallback if sibling not writable ]; foreach ($candidates as $d) { if (is_dir($d) || @mkdir($d, 0700, true)) { if (is_dir($d) && is_writable($d)) return $d; } } return $candidates[0]; } /** Write human-only vault material. PHP auth uses HASH only (ethos: vault not hot-path). */ function vault_write_operator(string $plainPass, array $walletSeeds = []): void { $d = vault_dir(); @chmod($d, 0700); $readme = "NOSIGNUP.TRADE VAULT - ROOT/OPERATOR ONLY\n" . "Not served by nginx (keep outside html/ or deny).\n" . "ADMIN_PASSWORD.txt = legacy key-combo operator console (Ctrl+Alt+Shift+N), NOT /controlpanel.\n" . "/controlpanel unlocks with panel site.seed (SITE_WALLET_SEED.txt); KING mint is treasury.secret + KING_FAUCET_SEED.txt.\n" . "Web auth stores hash in data/admin.pass.hash; clear admin password lives here for root pull.\n" . "Optional WALLET_SEEDS.txt only if operator seeds were passed at write time.\n" . "Site wallet seed does not rotate on yearly wipe; admin password is re-keyed at wipe.\n" . "Generated: " . gmdate('c') . "\n"; @file_put_contents($d . DIRECTORY_SEPARATOR . 'README.txt', $readme, LOCK_EX); @file_put_contents($d . DIRECTORY_SEPARATOR . 'ADMIN_PASSWORD.txt', $plainPass . "\n", LOCK_EX); @chmod($d . DIRECTORY_SEPARATOR . 'ADMIN_PASSWORD.txt', 0600); if ($walletSeeds) { $w = ''; foreach ($walletSeeds as $k => $v) $w .= strtoupper((string)$k) . "=" . trim((string)$v) . "\n"; @file_put_contents($d . DIRECTORY_SEPARATOR . 'WALLET_SEEDS.txt', $w, LOCK_EX); @chmod($d . DIRECTORY_SEPARATOR . 'WALLET_SEEDS.txt', 0600); } // deny via htaccess if vault accidentally under web root $ht = $d . DIRECTORY_SEPARATOR . '.htaccess'; if (!is_file($ht)) @file_put_contents($ht, "Require all denied\nDeny from all\n"); } function admin_pass_is_set(): bool { global $ADMIN_HASH_FILE, $ADMIN_PASS_FILE; if (is_file($ADMIN_HASH_FILE) && trim((string)file_get_contents($ADMIN_HASH_FILE)) !== '') return true; // legacy plaintext return is_file($ADMIN_PASS_FILE) && trim((string)file_get_contents($ADMIN_PASS_FILE)) !== ''; } function admin_pass_set(string $pass, bool $alsoVault = true): void { global $ADMIN_HASH_FILE, $ADMIN_PASS_FILE; $pass = trim($pass); if (strlen($pass) < 8) api_out(['ok' => false, 'err' => 'admin pass min 8 chars'], 400); $hash = password_hash($pass, PASSWORD_DEFAULT); if ($hash === false) api_out(['ok' => false, 'err' => 'hash fail'], 500); file_put_contents($ADMIN_HASH_FILE, $hash . "\n", LOCK_EX); @chmod($ADMIN_HASH_FILE, 0600); // remove legacy plaintext from web data if present if (is_file($ADMIN_PASS_FILE)) @unlink($ADMIN_PASS_FILE); if ($alsoVault) vault_write_operator($pass); } function admin_ok(?string $pass): bool { global $ADMIN_HASH_FILE, $ADMIN_PASS_FILE; if ($pass === null || $pass === '') return false; $pass = trim($pass); if (is_file($ADMIN_HASH_FILE)) { $h = trim((string)file_get_contents($ADMIN_HASH_FILE)); if ($h !== '' && password_verify($pass, $h)) return true; } // migrate legacy plaintext once if (is_file($ADMIN_PASS_FILE)) { $have = trim((string)file_get_contents($ADMIN_PASS_FILE)); if ($have !== '' && hash_equals($have, $pass)) { admin_pass_set($pass, true); return true; } } return false; } /** * Panel gate (U7 product path): site wallet seed (KING treasury OR LORD operator_addr) * OR one-release legacy admin_pass.hash migrate. No visitor login. No recovery. * Mint/faucet surfaces use require_faucet_auth() instead — renters never mint. */ function require_admin(): void { // Charter: admin secrets belong in POST bodies, never URLs (no GET seed/pass). $seed = (string)($_POST['seed'] ?? ''); if ($seed !== '' && panel_seed_ok($seed)) { return; } $p = (string)($_POST['admin_pass'] ?? ''); if (!admin_ok($p)) { api_out(['ok' => false, 'err' => 'admin auth'], 401); } } /** * Genesis: mint 24_000_000 NSU to KING treasury (MASTER FAUCET for this site). * E3: also birth a DISTINCT trade panel site.seed; operator.addr := panel addr (≠ treasury). * KING seed: data/treasury.secret + vault KING_FAUCET_SEED.txt (only minter). * Panel seed: data/site.seed + vault SITE_WALLET_SEED.txt (admin door; no mint). * Pre-E3 installs (had genesis, no site.seed): stay on treasury=panel conflation fallback. * Renters must NOT receive treasury.secret. Population fields metadata only. */ function ensure_genesis(): void { global $TFILE, $META, $CHAIN; boot_data(); $hadGenesis = false; foreach (read_chain() as $r) { if (($r['type'] ?? '') === 'genesis') { $hadGenesis = true; break; } } if ($hadGenesis) { // Do not invent site.seed on legacy empires (would orphan operator offline paper). nst_ensure_operator_addr(); nst_bootstrap_spread_if_empty(); return; } ensure_genesis_secret_only(); $sec = trim((string)file_get_contents($TFILE)); $taddr = addr_from_seed($sec); // Distinct panel wallet (one of 10 crop panels); never holds genesis mint. $panelSeed = ensure_panel_site_seed_only(); $panelAddr = $panelSeed !== '' ? addr_from_seed($panelSeed) : $taddr; $ts = time(); $popInfo = nst_world_population(); $pop = (int)$popInfo['pop']; $treasuryAmt = nst_optimum_treasury_micros($pop); // fail-open: if crawl returned absurd, still land a working treasury if ($treasuryAmt === '0' || !dec_ok($treasuryAmt)) { $treasuryAmt = NST_GENESIS_TREASURY; } $body = 'genesis|1|' . $treasuryAmt . '|' . $taddr . '|' . $pop . '|' . $ts; $row = [ 'type' => 'genesis', 'ver' => 2, 'ts' => $ts, 'max_supply' => $treasuryAmt, // soft: liquid scale = 24_000_000 NSU KING faucet at birth 'outputs' => [['addr' => $taddr, 'amount' => $treasuryAmt]], 'treasury_addr' => $taddr, 'panel_addr' => $panelAddr, 'population' => $pop, 'population_source' => (string)$popInfo['source'], 'optimum_micros_per_human' => NST_OPTIMUM_MICROS_PER_HUMAN, // legacy metadata only; not supply law // MASTER FAUCET: KING address holds genesis supply for THIS site only. // Panel site.seed is distinct (no mint). Renters must not receive treasury.secret. 'note' => 'NSU genesis MASTER FAUCET: treasury = 24_000_000 NSU on KING; panel site.seed distinct; rewards = treasury transfers only; yearly re-anchor on wipe; KING seed unlocks faucet', 'sig' => mac_sign($sec, $body), ]; append_row($row); $m = load_meta(); $m['version'] = NST_VERSION; $m['treasury'] = $taddr; $m['panel_addr'] = $panelAddr; $m['created'] = $m['created'] ?? $ts; $m['population'] = $pop; $m['population_source'] = (string)$popInfo['source']; $m['population_ts'] = (int)$popInfo['ts']; $m['treasury_target_micros'] = $treasuryAmt; $m['soft_max_supply_micros'] = $treasuryAmt; if (empty($m['epoch_start'])) { $m['epoch_start'] = $ts; $m['epoch_n'] = 1; } save_meta($m); // E3: site panel wallet is operator at birth; KING treasury is separate minter. nst_ensure_operator_addr($panelAddr); nst_vault_king_seed_note($sec); ensure_meta_epochs(); // Seed discovery book: loose BUY/SELL around temp 1.0 if no open orders. nst_bootstrap_spread_if_empty(); } function treasury_secret(): string { ensure_genesis(); global $TFILE; return trim((string)file_get_contents($TFILE)); } function treasury_addr(): string { return addr_from_seed(treasury_secret()); } function api_out(array $x, int $c = 200): void { http_response_code($c); header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: no-store'); echo j($x); exit; } function require_seed(): array { $seed = norm_seed((string)($_POST['seed'] ?? '')); $parts = explode(' ', $seed); if (count($parts) < 12) api_out(['ok' => false, 'err' => 'Need your 12-word seed'], 400); return [$seed, addr_from_seed($seed)]; } /* -------- Public source (?src=1 / ?download=1) - no auth, before APIs -------- */ if (isset($_GET['src']) || isset($_GET['download']) || (isset($_GET['api']) && (string)$_GET['api'] === 'src')) { $raw = (string)file_get_contents(__FILE__); header('Content-Type: text/plain; charset=UTF-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); header('X-NS-Sha256: ' . hash('sha256', $raw)); header('Content-Disposition: attachment; filename="index.php"'); header('Content-Length: ' . (string)strlen($raw)); echo $raw; exit; } /* -------- API -------- */ $api = $_GET['api'] ?? $_POST['api'] ?? ''; if ($api !== '') { ensure_genesis(); $chain = read_chain(); $bal = balances($chain); $taddr = treasury_addr(); // E2: honor freeze always; full recheck on force=1 or throttle expiry $forceConserve = ((string)($_GET['force'] ?? $_POST['force'] ?? '') === '1'); $nstConserveSnap = nst_conservation_boot_check($chain, $forceConserve); if ($api === 'state') { $orders = open_orders($chain); $bids = array_values(array_filter($orders, fn($o) => ($o['side'] ?? '') === 'buy')); $asks = array_values(array_filter($orders, fn($o) => ($o['side'] ?? '') === 'sell')); // Side-local sort (open_orders mixed comparator is not best-bid/best-ask safe). usort($bids, fn($a, $b) => dec_cmp((string)($b['price'] ?? '0'), (string)($a['price'] ?? '0'))); usort($asks, fn($a, $b) => dec_cmp((string)($a['price'] ?? '0'), (string)($b['price'] ?? '0'))); // Single source of P: same helper as SSR / buyback (never fixed sticker). $convergeP = book_mid_micros($chain); $mid = $convergeP !== null ? fmt_amt($convergeP) : null; $ep = epoch_info(); $meta = load_meta(); $pop = (int)($meta['population'] ?? 0); $target = (string)($meta['treasury_target_micros'] ?? ''); if ($target === '' || !dec_ok($target)) { $pi = nst_world_population(); $pop = (int)$pi['pop']; $target = nst_optimum_treasury_micros($pop); } $bootstrapSpreadActive = false; foreach ($orders as $o) { if (($o['kind'] ?? '') === 'bootstrap_spread') { $bootstrapSpreadActive = true; break; } } $bbPx = nst_buyback_price_micros($chain); api_out([ 'ok' => true, 'version' => NST_VERSION, 'price_policy' => 'market_converge', 'bootstrap_spread_active' => $bootstrapSpreadActive, 'buyback_note' => 'donate buyback arms at market P/2 (not fixed sticker)', 'market_p_fmt' => $mid, 'buyback_px_fmt' => $bbPx !== null ? fmt_amt($bbPx) : null, 'panel_unlock' => 'site_wallet_seed', // browser_hmac_v1: client HMAC with seed still on wire (fallback). // ecdsa_p256_v1: register_pubkey once, then signed ops omit seed. 'client_sign' => 'browser_hmac_or_ecdsa_p256', 'sig_mode' => 'ecdsa_p256_or_browser_hmac_or_server_mac', 'seed_on_wire' => 'unless_ecdsa_registered', 'seed_off_wire' => 'ecdsa_after_register_pubkey', 'asymmetric_plan' => 'ECDSA_P256_register_pubkey_then_seed_off_wire', 'ecdsa_spend' => 'register_pubkey_then_signed_ops_without_seed', 'ecdsa_ops' => ['transfer', 'order', 'cancel', 'fill', 'reputation'], 'wallet_inbox' => 'v0+autowire experimental · address-keyed sealed mail · mail not mint', 'inbox_e2e' => 'client_encrypt_server_ciphertext_only · experimental · not proven e2e', 'inbox_server_notice' => 'server-composed public book facts · encrypted for delivery · NOT private E2E', 'inbox_key_sep' => 'ECDSA_sign≠ECDH_enc (encpub_ vs pubkey_)', // Soft surface: hosts without OpenSSL cannot verify ECDSA (HMAC fallback still works). 'ecdsa_server' => nst_ecdsa_openssl_ready() ? 'openssl_available' : 'openssl_unavailable', 'ecdsa_verify_ready' => nst_ecdsa_openssl_ready(), 'max_supply_fmt' => fmt_amt((string)($meta['soft_max_supply_micros'] ?? NST_MAX_SUPPLY)), 'genesis_supply_nsu' => NST_KING_FAUCET_START_NSU, // fixed 24_000_000 NSU KING faucet 'supply_law' => 'fixed_24_000_000_NSU_KING_faucet; not live pop×1000; population/optimum_* metadata only', 'treasury_addr' => $taddr, 'treasury_bal_fmt' => fmt_amt($bal[$taddr] ?? '0'), 'treasury_target_fmt' => fmt_amt($target), 'population' => $pop, 'population_source' => (string)($meta['population_source'] ?? ''), 'optimum_nsu_per_human' => fmt_amt(NST_OPTIMUM_MICROS_PER_HUMAN), // legacy metadata only 'chain_len' => count($chain), 'book' => ['bids' => array_slice($bids, 0, 30), 'asks' => array_slice($asks, 0, 30), 'mid' => $mid], 'recent' => array_slice($chain, -50), 'epoch' => $ep, 'admin_pass_set' => admin_pass_is_set(), 'early_daily_fmt' => fmt_amt(NST_MOD_DAILY_MICROS), 'economy' => (function () use ($chain) { $e = load_economy(); $midM = book_mid_micros($chain); $bbM = nst_buyback_price_micros($chain); return [ 'profit_fmt' => fmt_amt((string)($e['profit_micros'] ?? '0')), 'ad_bid_fmt' => fmt_amt((string)($e['ad_bid_micros'] ?? '0')), 'mid_fmt' => $midM !== null ? fmt_amt($midM) : null, 'buyback_px_fmt' => $bbM !== null ? fmt_amt($bbM) : null, 'donate_k' => (string)($e['donate_k'] ?? NST_DONATE_K), 'value_note' => (string)($e['value_note'] ?? ''), 'value_authority' => 'this_server_owner', 'mirrors_role' => 'free_tributaries', ]; })(), 'conservation' => nst_conservation_public_view($nstConserveSnap), 'writes_frozen' => nst_conservation_is_frozen(), 'self_hash_hint' => 'sha256 of this index.php on disk - compute offline to verify release', ]); } if ($api === 'balance') { $a = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_REQUEST['addr'] ?? ''))); if (strlen($a) !== 64) api_out(['ok' => false, 'err' => 'bad addr'], 400); $b = $bal[$a] ?? '0'; $res = reserved_nsu($chain, $a); $av = available_nsu($bal, $chain, $a); $reps = rep_scores($chain); // history for addr $hist = []; foreach (array_reverse($chain) as $r) { if (($r['type'] ?? '') === 'transfer' && (($r['from'] ?? '') === $a || ($r['to'] ?? '') === $a)) { $hist[] = $r; if (count($hist) >= 40) break; } if (($r['type'] ?? '') === 'order' && ($r['addr'] ?? '') === $a) { $hist[] = $r; if (count($hist) >= 40) break; } if (($r['type'] ?? '') === 'fill' && (($r['maker'] ?? '') === $a || ($r['taker'] ?? '') === $a)) { $hist[] = $r; if (count($hist) >= 40) break; } if (($r['type'] ?? '') === 'reputation' && (($r['rater'] ?? '') === $a || ($r['rated'] ?? '') === $a)) { $hist[] = $r; if (count($hist) >= 40) break; } if (($r['type'] ?? '') === 'modifier_grant' && ($r['addr'] ?? '') === $a) { $hist[] = $r; if (count($hist) >= 40) break; } } $mods = load_mods(); $mod = $mods[$a] ?? null; api_out([ 'ok' => true, 'addr' => $a, 'balance' => $b, 'balance_fmt' => fmt_amt($b), 'reserved' => $res, 'reserved_fmt' => fmt_amt($res), 'available' => $av, 'available_fmt' => fmt_amt($av), 'rep_score' => $reps[$a] ?? 0, 'history' => $hist, 'has_modifier' => !empty($mod['permanent']), 'modifier' => $mod ? [ 'permanent' => !empty($mod['permanent']), 'daily_fmt' => fmt_amt((string)($mod['daily_micros'] ?? NST_MOD_DAILY_MICROS)), 'granted_ts' => (int)($mod['granted_ts'] ?? 0), 'granted_epoch' => (int)($mod['granted_epoch'] ?? 0), 'last_accrual_ts' => (int)($mod['last_accrual_ts'] ?? 0), ] : null, 'epoch' => epoch_info(), ]); } if ($api === 'addr' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); [$seed, $addr] = require_seed(); $grant = grant_early_modifier($addr); $acc = lazy_accrue($addr); api_out([ 'ok' => true, 'addr' => $addr, 'modifier_grant' => $grant, 'accrual' => $acc, 'epoch' => epoch_info(), ]); } if ($api === 'accrue' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); [$seed, $addr] = require_seed(); $grant = grant_early_modifier($addr); $acc = lazy_accrue($addr); $bal = balances(read_chain()); api_out([ 'ok' => true, 'addr' => $addr, 'modifier_grant' => $grant, 'accrual' => $acc, 'balance_fmt' => fmt_amt($bal[$addr] ?? '0'), 'available_fmt' => fmt_amt(available_nsu($bal, read_chain(), $addr)), 'has_modifier' => !empty(load_mods()[$addr]['permanent']), ]); } // Register ECDSA P-256 SPKI for legacy addr (seed proves ownership once; later spends can omit seed). if ($api === 'register_pubkey' && $_SERVER['REQUEST_METHOD'] === 'POST') { if (!nst_ecdsa_openssl_ready()) { api_out([ 'ok' => false, 'err' => 'ecdsa verify unavailable (openssl extension required)', 'ecdsa_server' => 'openssl_unavailable', ], 503); } [$seed, $addr] = require_seed(); $spkiHex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['pubkey'] ?? '')) ?? ''); $spki = @hex2bin($spkiHex); if ($spki === false || strlen($spki) < 50) { api_out(['ok' => false, 'err' => 'bad pubkey'], 400); } $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'clock skew — refresh and retry'], 400); } $proof = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['ecdsa_sig'] ?? '')) ?? ''); $body = implode('|', ['register_pubkey', '1', $addr, $spkiHex, (string)$ts]); if ($proof === '' || !nst_ecdsa_verify_p256($spki, $body, $proof)) { api_out(['ok' => false, 'err' => 'bad ecdsa proof for pubkey'], 400); } if (!nst_save_registered_spki($addr, $spki)) { api_out(['ok' => false, 'err' => 'disk'], 500); } api_out([ 'ok' => true, 'addr' => $addr, 'pubkey_registered' => true, 'sig_mode' => 'ecdsa_p256_v1', 'note' => 'Pubkey stored. Spend may omit seed when ECDSA path works (experimental · not proven e2e); HMAC/seed still real fallback', ]); } // --- Wallet Inbox v0: enc-pubkey registry + sealed mail (ciphertext only) --- if ($api === 'register_enc_pubkey' && $_SERVER['REQUEST_METHOD'] === 'POST') { [$seed, $addr] = require_seed(); $spkiHex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['enc_pubkey'] ?? $_POST['pubkey'] ?? '')) ?? ''); $spki = @hex2bin($spkiHex); if ($spki === false || strlen($spki) < 50) { api_out(['ok' => false, 'err' => 'bad enc pubkey'], 400); } if (!nst_save_registered_enc_spki($addr, $spki)) { api_out(['ok' => false, 'err' => 'disk'], 500); } api_out([ 'ok' => true, 'addr' => $addr, 'enc_pubkey_registered' => true, 'key_usage' => 'ECDH_P256_encrypt_only', 'distinct_from' => 'ECDSA_P256_sign (register_pubkey)', 'note' => 'Encryption SPKI stored. Seal/decrypt experimental · not proven e2e until isolated proof', 'mail_not_mint' => true, ]); } // Public fetch of encryption SPKI (needed by sender to seal; not a secret). if ($api === 'inbox_enc_pubkey') { $addr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_GET['addr'] ?? $_POST['addr'] ?? ''))) ?? ''; if (strlen($addr) !== 64) api_out(['ok' => false, 'err' => 'bad addr'], 400); $spki = nst_load_registered_enc_spki($addr); if ($spki === null) { api_out(['ok' => false, 'err' => 'no enc pubkey registered', 'addr' => $addr], 404); } api_out([ 'ok' => true, 'addr' => $addr, 'enc_pubkey' => bin2hex($spki), 'alg' => NST_INBOX_ALG, 'note' => 'Public encryption key only · experimental wallet inbox v0', ]); } /** * KING seal: accept PRE-ENCRYPTED blob only (ct + eph_pub + iv + sig). * Server NEVER accepts plaintext for storage. require_faucet_auth. * Mail ≠ mint: no chain row, no balance change. */ if ($api === 'inbox_seal' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_faucet_auth(); // Refuse accidental plaintext legs (honesty floor). foreach (['plaintext', 'note', 'body', 'message', 'msg'] as $ban) { if (isset($_POST[$ban]) && trim((string)$_POST[$ban]) !== '') { api_out([ 'ok' => false, 'err' => 'plaintext not accepted — encrypt client-side; server stores ciphertext only', 'honesty' => 'refuse_server_plaintext_seal', ], 400); } } $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['to'] ?? ''))) ?? ''; if (strlen($to) !== 64) api_out(['ok' => false, 'err' => 'bad to'], 400); if (nst_load_registered_enc_spki($to) === null) { api_out(['ok' => false, 'err' => 'recipient has no enc pubkey — they must unlock wallet once'], 400); } $from = strtolower(addr_from_seed(treasury_secret())); $id = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['id'] ?? ''))) ?? ''; if ($id === '') $id = bin2hex(random_bytes(8)); if (strlen($id) < 8 || strlen($id) > 64) api_out(['ok' => false, 'err' => 'bad id'], 400); $ts = (int)($_POST['ts'] ?? time()); if (abs($ts - time()) > 600) api_out(['ok' => false, 'err' => 'clock skew — refresh and retry'], 400); $ttl = (int)($_POST['ttl'] ?? NST_INBOX_TTL_SECS); if ($ttl < 60) $ttl = 60; if ($ttl > NST_INBOX_TTL_SECS * 4) $ttl = NST_INBOX_TTL_SECS * 4; $exp = $ts + $ttl; $eph = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['eph_pub'] ?? '')) ?? ''); $iv = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['iv'] ?? '')) ?? ''); $ct = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['ct'] ?? '')) ?? ''); if (strlen($eph) < 100 || strlen($iv) !== 24 || strlen($ct) < 32) { api_out(['ok' => false, 'err' => 'bad ciphertext fields (eph_pub/iv/ct)'], 400); } if (strlen($ct) > NST_INBOX_MAX_CT_HEX) api_out(['ok' => false, 'err' => 'ct too large'], 400); $blob = [ 'v' => 1, 'id' => $id, 'to' => $to, 'from' => $from, 'ts' => $ts, 'ttl' => $ttl, 'exp' => $exp, 'eph_pub' => $eph, 'iv' => $iv, 'ct' => $ct, 'alg' => NST_INBOX_ALG, 'kind' => 'king_note', 'mail_not_mint' => true, 'honesty' => 'experimental · sealed wallet mail · server ciphertext only · not proven e2e', ]; $signBody = nst_inbox_sign_body($blob); $sig = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['sig'] ?? $_POST['ecdsa_sig'] ?? '')) ?? ''); $sigMode = strtolower(trim((string)($_POST['sig_mode'] ?? ''))); $spkiHex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['pubkey'] ?? '')) ?? ''); if ($sigMode === 'ecdsa_p256_v1' || ($sig !== '' && $spkiHex !== '' && nst_ecdsa_openssl_ready())) { $spki = @hex2bin($spkiHex); if ($spki === false || strlen($spki) < 50) api_out(['ok' => false, 'err' => 'bad sender pubkey'], 400); $reg = nst_load_registered_spki($from); if ($reg === null || !hash_equals($reg, $spki)) { // Allow seal-time register: if KING posts matching seed-proven from, require proof over register body first is separate. // Here: if no registry, accept SPKI only when ECDSA verifies AND from matches treasury (KING seed already checked). if ($reg !== null && !hash_equals($reg, $spki)) { api_out(['ok' => false, 'err' => 'sender pubkey not registered for treasury addr'], 403); } if ($reg === null) { nst_save_registered_spki($from, $spki); } } if ($sig === '' || !nst_ecdsa_verify_p256($spki, $signBody, $sig)) { api_out(['ok' => false, 'err' => 'bad ecdsa seal signature'], 400); } $blob['sig'] = $sig; $blob['sig_mode'] = 'ecdsa_p256_v1'; $blob['sender_spki'] = bin2hex($spki); } else { // Experimental HMAC fallback (seed already on wire for faucet auth) — labeled. $seed = (string)($_POST['seed'] ?? ''); $expect = mac_sign($seed, $signBody); if ($sig !== '' && !hash_equals($expect, $sig)) { api_out(['ok' => false, 'err' => 'bad hmac seal signature'], 400); } if ($sig === '') $sig = $expect; $blob['sig'] = $sig; $blob['sig_mode'] = 'server_mac'; $blob['honesty'] = 'experimental · sealed mail · HMAC seal (seed on wire for KING auth) · ciphertext-only store · not proven e2e'; } $path = nst_inbox_blob_path($to, $id); if (@file_put_contents($path, j($blob), LOCK_EX) === false) { api_out(['ok' => false, 'err' => 'disk'], 500); } api_out([ 'ok' => true, 'id' => $id, 'to' => $to, 'from' => $from, 'exp' => $exp, 'stored' => basename($path), 'mail_not_mint' => true, 'server_plaintext' => false, 'sig_mode' => $blob['sig_mode'], 'honesty' => $blob['honesty'], 'note' => 'Opaque ciphertext stored only · recipient decrypts client-side with seed-derived ECDH key', ]); } // List own sealed blobs (seed or ECDSA actor). if ($api === 'inbox_list' && $_SERVER['REQUEST_METHOD'] === 'POST') { $actor = nst_actor('addr'); $addr = $actor['addr']; // Light auth: list is not secret metadata-only; still require ownership of addr. if (!empty($actor['ecdsa'])) { $body = implode('|', ['inbox_list', '1', $addr, (string)(int)($_POST['ts'] ?? time())]); nst_auth_sig($actor, $body); } $items = nst_inbox_list_for_addr($addr); api_out([ 'ok' => true, 'addr' => $addr, 'count' => count($items), 'items' => $items, 'mail_not_mint' => true, 'honesty' => 'experimental · list returns metadata + ids; ct only via inbox_fetch · not proven e2e', ]); } // Fetch one sealed blob (opaque fields for client decrypt). if ($api === 'inbox_fetch' && $_SERVER['REQUEST_METHOD'] === 'POST') { $actor = nst_actor('addr'); $addr = $actor['addr']; $id = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['id'] ?? ''))) ?? ''; if ($id === '') api_out(['ok' => false, 'err' => 'need id'], 400); if (!empty($actor['ecdsa'])) { $body = implode('|', ['inbox_fetch', '1', $addr, $id, (string)(int)($_POST['ts'] ?? time())]); nst_auth_sig($actor, $body); } $path = nst_inbox_blob_path($addr, $id); if (!is_file($path)) { $path = nst_inbox_blob_path_legacy($addr, $id); } if (!is_file($path)) api_out(['ok' => false, 'err' => 'not found'], 404); $j = json_decode((string)@file_get_contents($path), true); if (!is_array($j) || (string)($j['to'] ?? '') !== $addr) { api_out(['ok' => false, 'err' => 'not found'], 404); } $exp = (int)($j['exp'] ?? 0); if ($exp > 0 && $exp < time()) { @unlink($path); api_out(['ok' => false, 'err' => 'expired'], 410); } api_out([ 'ok' => true, 'blob' => [ 'v' => (int)($j['v'] ?? 1), 'id' => (string)($j['id'] ?? ''), 'to' => (string)($j['to'] ?? ''), 'from' => (string)($j['from'] ?? ''), 'ts' => (int)($j['ts'] ?? 0), 'exp' => $exp, 'eph_pub' => (string)($j['eph_pub'] ?? ''), 'iv' => (string)($j['iv'] ?? ''), 'ct' => (string)($j['ct'] ?? ''), 'sig' => (string)($j['sig'] ?? ''), 'sig_mode' => (string)($j['sig_mode'] ?? ''), 'sender_spki' => (string)($j['sender_spki'] ?? ''), 'alg' => (string)($j['alg'] ?? NST_INBOX_ALG), 'kind' => (string)($j['kind'] ?? 'king_note'), 'honesty' => (string)($j['honesty'] ?? 'experimental'), ], 'mail_not_mint' => true, 'server_plaintext' => false, 'note' => 'Decrypt client-side with seed-derived ECDH key · experimental · not proven e2e', ]); } if ($api === 'transfer' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['to'] ?? ''))); $micros = parse_amt(trim((string)($_POST['amount'] ?? ''))); $memo = substr(trim((string)($_POST['memo'] ?? '')), 0, 120); $nonce = substr(trim((string)($_POST['nonce'] ?? bin2hex(random_bytes(8)))), 0, 64); if (strlen($to) !== 64) api_out(['ok' => false, 'err' => 'bad to'], 400); if ($micros === null || dec_cmp($micros, '1') < 0) api_out(['ok' => false, 'err' => 'bad amount'], 400); $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'clock skew — refresh and retry'], 400); } // Dual-path: ECDSA seed-off-wire OR seed + optional browser HMAC. $actor = nst_actor('from'); $from = $actor['addr']; if ($from === $to) api_out(['ok' => false, 'err' => 'self'], 400); $body = implode('|', ['transfer', '1', $from, $to, $micros, $nonce, (string)$ts, $memo]); $auth = nst_auth_sig($actor, $body); $sigMode = $auth['sig_mode']; $rowSig = $auth['sig']; $spkiHex = $auth['pubkey']; $risk = nst_brain_memo_risk($memo); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $to, 'amount' => $micros, 'amount_fmt' => fmt_amt($micros), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => $rowSig, 'sig_mode' => $sigMode, ]; if ($sigMode === 'ecdsa_p256_v1' && $spkiHex !== '') { $row['pubkey'] = $spkiHex; } if (!empty($risk['flag'])) $row['memo_risk'] = $risk; // Exclusive RMW: concurrent transfers cannot both pass available_nsu and overdraw. $xferOut = nst_with_chain_lock(function (array $chain, callable $append) use ($from, $to, $micros, $nonce, $row) { foreach ($chain as $r) { if (($r['type'] ?? '') === 'transfer' && ($r['from'] ?? '') === $from && ($r['nonce'] ?? '') === $nonce) { return ['ok' => false, 'http' => 400, 'err' => 'nonce replay']; } } $bal = balances($chain); $av = available_nsu($bal, $chain, $from); if (dec_cmp($av, $micros) < 0) { return [ 'ok' => false, 'http' => 400, 'err' => 'insufficient available', 'have_fmt' => fmt_amt($bal[$from] ?? '0'), 'available_fmt' => fmt_amt($av), ]; } if (!$append($row)) { return ['ok' => false, 'http' => 500, 'err' => 'disk']; } return [ 'ok' => true, 'tx' => $row, 'balance_fmt' => fmt_amt(dec_sub($bal[$from] ?? '0', $micros)), ]; }); if ($xferOut === null) { api_out(['ok' => false, 'err' => 'disk'], 500); } if (empty($xferOut['ok'])) { $http = (int)($xferOut['http'] ?? 400); unset($xferOut['http']); api_out($xferOut, $http); } // P3c/d: rent memo → try settle; on refundable failure, auto-refund payer from treasury. $txRow = is_array($xferOut['tx'] ?? null) ? $xferOut['tx'] : $row; $rentSettle = nst_rent_try_settle_transfer($txRow); $rentRefund = null; if (empty($rentSettle['settled']) && nst_rent_refundable_reason((string)($rentSettle['reason'] ?? ''))) { $rentRefund = nst_rent_auto_refund($txRow, $rentSettle); } // Rent notices AFTER settle/refund commit; fail-open (public lease facts). try { if (!empty($rentSettle['settled'])) { nst_inbox_notify_rent_settle($rentSettle); } if (is_array($rentRefund) && !empty($rentRefund['refunded'])) { nst_inbox_notify_rent_refund($rentRefund); } } catch (Throwable $e) { // never abort a committed transfer/rent op } api_out([ 'ok' => true, 'tx' => $xferOut['tx'], 'sig_mode' => $sigMode, 'seed_on_wire' => $auth['seed_on_wire'], 'balance_fmt' => $xferOut['balance_fmt'], 'brains' => ['memo_risk' => $risk], 'rent' => $rentSettle, 'rent_refund' => $rentRefund, ]); } /** Free claim retired — always 410. No mint, no transfer. */ if ($api === 'faucet' && $_SERVER['REQUEST_METHOD'] === 'POST') { api_out([ 'ok' => false, 'err' => 'Free NSU claim retired — buy NSU on the order book (Trade book tab)', 'law' => 'OPERATOR_GO 2026-07-16: KING path SELL/BUY only; free giveaway OUT. ' . 'Site use stays free (wallet, view, post). NSU is the tradeable coin, not a free drip.', 'free_claim' => 'retired', 'next' => 'place a buy order on the book · ?api=order side=buy', ], 410); } if ($api === 'order' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('addr'); $addr = $actor['addr']; $side = strtolower(trim((string)($_POST['side'] ?? ''))); $pair = strtoupper(trim((string)($_POST['pair'] ?? 'NSU/NOTE'))); $amount = parse_amt(trim((string)($_POST['amount'] ?? ''))); $price = parse_amt(trim((string)($_POST['price'] ?? ''))); $expiry = (int)($_POST['expiry'] ?? (time() + 86400)); if (!in_array($side, ['buy', 'sell'], true)) api_out(['ok' => false, 'err' => 'side'], 400); if ($amount === null || $price === null) api_out(['ok' => false, 'err' => 'amount/price'], 400); $sanity = nst_brain_order_sanity($side, $amount, $price); $pairBrain = nst_brain_pair_sane($pair); if ($expiry < time() + 60) $expiry = time() + 3600; if ($expiry > time() + 30 * 86400) $expiry = time() + 30 * 86400; // open-count + sell available re-checked under chain RMW lock below $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'clock skew — refresh and retry'], 400); } $idIn = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['order_id'] ?? ''))) ?? ''; if (strlen($idIn) === 64) { $id = $idIn; } else { $id = hash('sha256', $addr . '|' . $ts . '|' . $side . '|' . $amount . '|' . $price . '|' . bin2hex(random_bytes(4))); } $body = implode('|', ['order', '1', $id, $addr, $side, $pair, $amount, $price, (string)$expiry, (string)$ts]); $auth = nst_auth_sig($actor, $body); $sigMode = $auth['sig_mode']; $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $addr, 'side' => $side, 'pair' => $pair, 'amount' => $amount, 'amount_fmt' => fmt_amt($amount), 'price' => $price, 'price_fmt' => fmt_amt($price), 'expiry' => $expiry, 'ts' => $ts, 'sig' => $auth['sig'], 'sig_mode' => $sigMode, ]; if ($sigMode === 'ecdsa_p256_v1' && $auth['pubkey'] !== '') { $row['pubkey'] = $auth['pubkey']; } if (!empty($sanity['flag'])) $row['order_sanity'] = $sanity; if (!empty($pairBrain['flag'])) $row['pair_sane'] = $pairBrain; // Exclusive RMW: concurrent sells cannot over-reserve; open-count + id uniqueness under lock. // OPERATOR_GO §3c: treasury/KING SELL on book immediately arms BID @ half (same pair). // Pre-read treasury material OUTSIDE chain lock (treasury_secret → ensure_genesis can append_row). $tsecForHalf = ''; $taddrForHalf = ''; try { $tsecForHalf = treasury_secret(); $taddrForHalf = strtolower(addr_from_seed($tsecForHalf)); } catch (Throwable $e) { $tsecForHalf = ''; $taddrForHalf = ''; } $isTreasurySell = ($side === 'sell' && $taddrForHalf !== '' && strtolower($addr) === $taddrForHalf && (($row['kind'] ?? '') !== 'bootstrap_spread')); $orderOut = nst_with_chain_lock(function (array $chain, callable $append) use ( $addr, $side, $pair, $amount, $id, $row, $isTreasurySell, $expiry, $ts, $tsecForHalf, $taddrForHalf ) { foreach ($chain as $r) { if (($r['type'] ?? '') === 'order' && ($r['id'] ?? '') === $id) { return ['ok' => false, 'http' => 400, 'err' => 'order_id already used']; } } $n = 0; foreach (open_orders($chain) as $o) { if (($o['addr'] ?? '') === $addr) $n++; } // Treasury SELL needs a free slot for the paired half-BID (two posts). $slotCap = $isTreasurySell ? 19 : 20; if ($n >= $slotCap) { return ['ok' => false, 'http' => 400, 'err' => 'too many open orders']; } if ($side === 'sell' && str_starts_with($pair, 'NSU')) { $bal = balances($chain); $av = available_nsu($bal, $chain, $addr); if (dec_cmp($av, $amount) < 0) { return [ 'ok' => false, 'http' => 400, 'err' => 'insufficient available NSU', 'available_fmt' => fmt_amt($av), ]; } } if (!$append($row)) { return ['ok' => false, 'http' => 500, 'err' => 'disk']; } $halfBid = null; if ($isTreasurySell) { $hb = nst_treasury_half_bid_from_sell( (string)$id, (string)$pair, (string)$amount, (string)($row['price'] ?? '0'), (int)$expiry, (int)$ts, $chain, $append, (string)$tsecForHalf, (string)$taddrForHalf ); if (empty($hb['ok'])) { return [ 'ok' => false, 'http' => 500, 'err' => 'treasury half-bid failed after sell', 'detail' => $hb['err'] ?? 'unknown', 'order' => $row, ]; } $halfBid = $hb['order'] ?? null; } return ['ok' => true, 'order' => $row, 'half_bid' => $halfBid]; }); if ($orderOut === null) { api_out(['ok' => false, 'err' => 'disk'], 500); } if (empty($orderOut['ok'])) { $http = (int)($orderOut['http'] ?? 400); unset($orderOut['http']); api_out($orderOut, $http); } $outPayload = [ 'ok' => true, 'order' => $orderOut['order'], 'sig_mode' => $sigMode, 'seed_on_wire' => $auth['seed_on_wire'], 'brains' => ['order_sanity' => $sanity, 'pair_sane' => $pairBrain], ]; if (!empty($orderOut['half_bid'])) { $outPayload['half_bid'] = $orderOut['half_bid']; $outPayload['half_bid_note'] = 'OPERATOR_GO §3c: treasury SELL armed paired BID @ half price'; } // Half-BID armed notice: AFTER chain write, fail-open (public book floor). try { if (!empty($orderOut['half_bid']) && is_array($orderOut['half_bid'])) { $sellRow = is_array($orderOut['order'] ?? null) ? $orderOut['order'] : null; nst_inbox_notify_half_bid($sellRow, $orderOut['half_bid']); } } catch (Throwable $e) { // never abort a committed order/half-BID } api_out($outPayload); } if ($api === 'cancel' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('addr'); $addr = $actor['addr']; $oid = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['order_id'] ?? ''))); if (strlen($oid) !== 64) api_out(['ok' => false, 'err' => 'bad order_id'], 400); $chain = read_chain(); $found = null; foreach ($chain as $r) { if (($r['type'] ?? '') === 'order' && ($r['id'] ?? '') === $oid) $found = $r; } if (!$found) api_out(['ok' => false, 'err' => 'not found'], 404); if (($found['addr'] ?? '') !== $addr) api_out(['ok' => false, 'err' => 'not owner'], 403); // Honesty: only open book rows may cancel (filled/cancelled/expired → not open). // Not a fund-lock invent: fill already rechecks open_orders under nst_with_chain_lock. $stillOpen = false; foreach (open_orders($chain) as $oLive) { if (($oLive['id'] ?? '') === $oid) { $stillOpen = true; break; } } if (!$stillOpen) { api_out(['ok' => false, 'err' => 'order not open'], 404); } $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'clock skew — refresh and retry'], 400); } $body = implode('|', ['cancel', '1', $oid, $addr, (string)$ts]); $auth = nst_auth_sig($actor, $body); $sigMode = $auth['sig_mode']; $row = [ 'type' => 'cancel', 'ver' => 1, 'order_id' => $oid, 'addr' => $addr, 'ts' => $ts, 'sig' => $auth['sig'], 'sig_mode' => $sigMode, ]; if ($sigMode === 'ecdsa_p256_v1' && $auth['pubkey'] !== '') { $row['pubkey'] = $auth['pubkey']; } if (!append_row($row)) api_out(['ok' => false, 'err' => 'disk'], 500); api_out(['ok' => true, 'cancel' => $row, 'sig_mode' => $sigMode, 'seed_on_wire' => $auth['seed_on_wire']]); } // Take a live order: move NSU leg when pair starts with NSU // sell order: maker sells NSU -> transfer maker->taker // buy order: maker buys NSU -> transfer taker->maker (taker must hold NSU) // Chain exclusive RMW under LOCK_EX so concurrent fills cannot over-debit maker. if ($api === 'fill' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('taker'); $taker = $actor['addr']; $oid = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['order_id'] ?? ''))); $qtyH = trim((string)($_POST['amount'] ?? '')); $settle = substr(trim((string)($_POST['settle_ref'] ?? '')), 0, 160); if (strlen($oid) !== 64) api_out(['ok' => false, 'err' => 'bad order_id'], 400); // Pre-read for auth body qty (signatures bind amount). Remaining re-checked under lock. $chainPeek = read_chain(); $ordersPeek = open_orders($chainPeek); $foundPeek = null; foreach ($ordersPeek as $o) if (($o['id'] ?? '') === $oid) { $foundPeek = $o; break; } if (!$foundPeek) api_out(['ok' => false, 'err' => 'order not open'], 404); // Bootstrap BUY/SELL are temp mid discovery only — never a free NSU faucet path. if (($foundPeek['kind'] ?? '') === 'bootstrap_spread') { api_out([ 'ok' => false, 'err' => 'Bootstrap discovery quote — not fillable. Post your own order or take a real maker.', 'kind' => 'bootstrap_spread', ], 400); } $maker = (string)($foundPeek['addr'] ?? ''); if ($maker === $taker) api_out(['ok' => false, 'err' => 'self fill'], 400); $remPeek = $foundPeek['remaining'] ?? '0'; $qty = $qtyH === '' ? $remPeek : parse_amt($qtyH); if ($qty === null || dec_cmp($qty, '1') < 0) api_out(['ok' => false, 'err' => 'bad amount'], 400); if (dec_cmp($qty, $remPeek) > 0) api_out(['ok' => false, 'err' => 'over remaining', 'remaining_fmt' => fmt_amt($remPeek)], 400); $pair = strtoupper((string)($foundPeek['pair'] ?? '')); $side = (string)($foundPeek['side'] ?? ''); $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'clock skew — refresh and retry'], 400); } $nonce = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['nonce'] ?? ''))) ?? ''; if (strlen($nonce) < 8) { $nonce = bin2hex(random_bytes(8)); } $memo = 'fill:' . substr($oid, 0, 12); $tx = null; $txAuth = null; if (str_starts_with($pair, 'NSU')) { if ($side === 'sell') { $from = $maker; $to = $taker; } elseif ($side === 'buy') { $from = $taker; $to = $maker; } else { api_out(['ok' => false, 'err' => 'side'], 400); } $tbody = implode('|', ['transfer', '1', $from, $to, $qty, $nonce, (string)$ts, $memo]); // For sell: maker already authorized via open sell order sig. // For buy: taker spends — ECDSA or browser HMAC on transfer body (signed before lock). if ($side === 'sell') { $tx = [ 'type' => 'transfer', 'ver' => 1, 'from' => $maker, 'to' => $taker, 'amount' => $qty, 'amount_fmt' => fmt_amt($qty), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => $foundPeek['sig'] ?? '', 'auth' => 'open_sell_order', 'order_id' => $oid, ]; } else { // ECDSA: require dedicated tx_ecdsa_sig (do not reuse fill ecdsa_sig). $txEcdsaOverride = !empty($actor['ecdsa']) ? strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['tx_ecdsa_sig'] ?? '')) ?? '') : null; $txAuth = nst_auth_sig($actor, $tbody, 'tx_client_sig', $txEcdsaOverride); $tx = [ 'type' => 'transfer', 'ver' => 1, 'from' => $taker, 'to' => $maker, 'amount' => $qty, 'amount_fmt' => fmt_amt($qty), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => $txAuth['sig'], 'sig_mode' => $txAuth['sig_mode'], 'auth' => $txAuth['sig_mode'] === 'ecdsa_p256_v1' ? 'taker_ecdsa' : 'taker_seed', 'order_id' => $oid, ]; if ($txAuth['sig_mode'] === 'ecdsa_p256_v1' && $txAuth['pubkey'] !== '') { $tx['pubkey'] = $txAuth['pubkey']; } } } $fbody = implode('|', ['fill', '1', $oid, $maker, $taker, $qty, $settle, (string)$ts]); $fillAuth = nst_auth_sig($actor, $fbody); $sigMode = $fillAuth['sig_mode']; $fillOut = nst_with_chain_lock(function (array $chain, callable $append) use ( $oid, $taker, $maker, $qty, $settle, $ts, $pair, $side, $tx, $fillAuth, $sigMode, $nonce ) { $orders = open_orders($chain); $found = null; foreach ($orders as $o) if (($o['id'] ?? '') === $oid) { $found = $o; break; } if (!$found) { return ['ok' => false, 'http' => 404, 'err' => 'order not open']; } if (($found['kind'] ?? '') === 'bootstrap_spread') { return [ 'ok' => false, 'http' => 400, 'err' => 'Bootstrap discovery quote — not fillable. Post your own order or take a real maker.', 'kind' => 'bootstrap_spread', ]; } if ((string)($found['addr'] ?? '') !== $maker) { return ['ok' => false, 'http' => 400, 'err' => 'order changed — refresh book']; } if ((string)($found['side'] ?? '') !== $side || strtoupper((string)($found['pair'] ?? '')) !== $pair) { return ['ok' => false, 'http' => 400, 'err' => 'order changed — refresh book']; } $rem = $found['remaining'] ?? '0'; if (dec_cmp($qty, $rem) > 0) { return ['ok' => false, 'http' => 400, 'err' => 'over remaining', 'remaining_fmt' => fmt_amt($rem)]; } // Reject identical fill packet replay (sell leg has no client-bound nonce on fill body). foreach ($chain as $r) { if (($r['type'] ?? '') !== 'fill') continue; if (($r['order_id'] ?? '') !== $oid) continue; if (($r['taker'] ?? '') !== $taker) continue; if ((int)($r['ts'] ?? 0) !== $ts) continue; if (dec_norm((string)($r['amount'] ?? '0')) !== dec_norm($qty)) continue; return ['ok' => false, 'http' => 400, 'err' => 'fill replay — refresh book']; } $txRow = $tx; if (str_starts_with($pair, 'NSU')) { $bal = balances($chain); if ($side === 'sell') { $have = $bal[$maker] ?? '0'; if (dec_cmp($have, $qty) < 0) { return ['ok' => false, 'http' => 400, 'err' => 'maker lacks NSU']; } } elseif ($side === 'buy') { $av = available_nsu($bal, $chain, $taker); if (dec_cmp($av, $qty) < 0) { return ['ok' => false, 'http' => 400, 'err' => 'insufficient available NSU', 'available_fmt' => fmt_amt($av)]; } } // Same nonce uniqueness as transfer API (buy-leg replay must not re-spend). if ($txRow) { $txFrom = (string)($txRow['from'] ?? ''); $txNonce = (string)($txRow['nonce'] ?? $nonce); if ($txFrom !== '' && $txNonce !== '') { foreach ($chain as $r) { if (($r['type'] ?? '') === 'transfer' && ($r['from'] ?? '') === $txFrom && ($r['nonce'] ?? '') === $txNonce) { return ['ok' => false, 'http' => 400, 'err' => 'nonce replay']; } } } // Refresh sell-order sig from live row under lock (maker re-auth not required). if ($side === 'sell') { $txRow['sig'] = $found['sig'] ?? ($txRow['sig'] ?? ''); } if (!$append($txRow)) { return ['ok' => false, 'http' => 500, 'err' => 'disk tx']; } } } $fill = [ 'type' => 'fill', 'ver' => 1, 'order_id' => $oid, 'maker' => $maker, 'taker' => $taker, 'side' => $side, 'pair' => $pair, 'amount' => $qty, 'amount_fmt' => fmt_amt($qty), 'price' => $found['price'] ?? '0', 'price_fmt' => $found['price_fmt'] ?? '', 'settle_ref' => $settle, 'ts' => $ts, 'taker_sig' => $fillAuth['sig'], 'sig_mode' => $sigMode, ]; if ($sigMode === 'ecdsa_p256_v1' && $fillAuth['pubkey'] !== '') { $fill['pubkey'] = $fillAuth['pubkey']; } if (!$append($fill)) { return ['ok' => false, 'http' => 500, 'err' => 'disk fill']; } return [ 'ok' => true, 'fill' => $fill, 'transfer' => $txRow, 'sig_mode' => $sigMode, 'seed_on_wire' => $fillAuth['seed_on_wire'], ]; }); if ($fillOut === null) { api_out(['ok' => false, 'err' => 'disk fill'], 500); } if (empty($fillOut['ok'])) { $http = (int)($fillOut['http'] ?? 400); unset($fillOut['http']); api_out($fillOut, $http); } // Money-adjacent server_notice: AFTER fill commits, OUTSIDE chain lock, fail-open. try { if (is_array($fillOut['fill'] ?? null)) { nst_inbox_notify_fill($fillOut['fill']); } } catch (Throwable $e) { // never abort a committed fill } api_out($fillOut); } if ($api === 'reputation' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('rater'); $rater = $actor['addr']; $rated = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['rated'] ?? ''))); $score = (int)($_POST['score'] ?? 0); $ctx = substr(trim((string)($_POST['context'] ?? '')), 0, 160); if (strlen($rated) !== 64) api_out(['ok' => false, 'err' => 'bad rated'], 400); if ($rated === $rater) api_out(['ok' => false, 'err' => 'self'], 400); if ($score < -10 || $score > 10) api_out(['ok' => false, 'err' => 'score -10..10'], 400); $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'clock skew — refresh and retry'], 400); } $body = implode('|', ['reputation', '1', $rater, $rated, (string)$score, $ctx, (string)$ts]); $auth = nst_auth_sig($actor, $body); $sigMode = $auth['sig_mode']; $row = [ 'type' => 'reputation', 'ver' => 1, 'rater' => $rater, 'rated' => $rated, 'score' => $score, 'context' => $ctx, 'ts' => $ts, 'sig' => $auth['sig'], 'sig_mode' => $sigMode, ]; if ($sigMode === 'ecdsa_p256_v1' && $auth['pubkey'] !== '') { $row['pubkey'] = $auth['pubkey']; } if (!append_row($row)) api_out(['ok' => false, 'err' => 'disk'], 500); $scores = rep_scores(read_chain()); api_out([ 'ok' => true, 'edge' => $row, 'rated_total' => $scores[$rated] ?? 0, 'sig_mode' => $sigMode, 'seed_on_wire' => $auth['seed_on_wire'], ]); } if ($api === 'audit') { $n = max(1, min(500, (int)($_GET['n'] ?? 100))); $chain = read_chain(); $rep = nst_conservation_check_and_maybe_freeze($chain); api_out([ 'ok' => true, 'n' => count($chain), 'tail' => array_slice($chain, -$n), 'conservation' => [ 'ok' => !empty($rep['ok']), 'frozen' => !empty($rep['frozen']) || nst_conservation_is_frozen(), 'sum_fmt' => fmt_amt((string)$rep['observed_sum']), 'expected_fmt' => fmt_amt((string)$rep['expected_issued']), 'checked_ts' => (int)$rep['checked_ts'], 'int_ok' => !empty($rep['int_ok']), 'detail' => $rep['detail'] ?? null, ], ]); } if ($api === 'conservation') { $chain = read_chain(); $force = ((string)($_GET['force'] ?? $_POST['force'] ?? '1') !== '0'); $rep = $force ? nst_conservation_check_and_maybe_freeze($chain) : null; if ($rep === null) { $snap = nst_conservation_boot_check($chain, false); api_out(['ok' => true, 'conservation' => nst_conservation_public_view($snap)]); } api_out([ 'ok' => true, 'conservation' => [ 'ok' => !empty($rep['ok']), 'frozen' => !empty($rep['frozen']) || nst_conservation_is_frozen(), 'sum_fmt' => fmt_amt((string)$rep['observed_sum']), 'expected_fmt' => fmt_amt((string)$rep['expected_issued']), 'checked_ts' => (int)$rep['checked_ts'], 'int_ok' => !empty($rep['int_ok']), 'detail' => $rep['detail'] ?? null, 'freeze_path_basename' => 'conservation.freeze.json', ], ]); } if ($api === 'selfhash') { api_out([ 'ok' => true, 'version' => NST_VERSION, 'sha256' => @hash_file('sha256', __FILE__) ?: null, 'file' => basename(__FILE__), ]); } /* ---- Operator console (key-combo UI + password; secrets in data/*.txt) ---- */ if ($api === 'admin_status') { $eStat = load_economy(); $opStat = strtolower(trim((string)($eStat['operator_addr'] ?? ''))); $rentedStat = !empty($eStat['rented']) && $opStat !== '' && $opStat !== strtolower($taddr); api_out([ 'ok' => true, 'admin_pass_set' => admin_pass_is_set(), 'site' => 'trade', 'version' => NST_VERSION, 'epoch' => epoch_info(), 'mods_count' => count(load_mods()), 'treasury_addr' => $taddr, 'treasury_bal_fmt' => fmt_amt($bal[$taddr] ?? '0'), 'chain_len' => count($chain), 'rented' => $rentedStat, 'operator_addr' => $opStat !== '' ? $opStat : $taddr, 'lord_rent_grant' => 'retired', 'lord_rent_credit_legacy' => null, 'rent_quote_api' => 'rent_quote', 'rent_quote_get_api' => 'rent_quote_get', 'rent_settle_api' => 'rent_settle', 'rent_quotes_durable' => true, 'rent_settle' => 'transfer_memo_first_paid_wins', 'rent_nsu_per_day' => (int)NST_RENT_NSU_PER_DAY, 'lord_rent_formula' => [ 'status' => 'retired_grant_path', 'law' => 'rent is payment TO treasury (NSU_VIABILITY_V1); ?api=rent_quote live; pay-bind settle next', 'nsu_per_day' => (int)NST_RENT_NSU_PER_DAY, 'faucet_start_nsu_narrative' => NST_KING_FAUCET_START_NSU, ], 'last_epoch_profit_fmt' => fmt_amt((string)($eStat['last_epoch_profit_micros'] ?? '0')), 'panel_door' => 'E3: trade panel site.seed or KING treasury (owner) or LORD operator_addr; faucet=KING only; legacy admin.pass.hash migrate only', ]); } if ($api === 'admin_setup' && $_SERVER['REQUEST_METHOD'] === 'POST') { if (admin_pass_is_set()) { api_out(['ok' => false, 'err' => 'already set - use admin_login'], 400); } // KING-only: LORD must not mint a panel password that looks like operator authority. // Seed exists at genesis — no unauthenticated land-grab of the legacy password door. $seed = (string)($_POST['seed'] ?? ''); if ($seed === '' || !treasury_seed_ok($seed)) { api_out(['ok' => false, 'err' => 'admin auth'], 401); } $pass = (string)($_POST['admin_pass'] ?? ''); admin_pass_set($pass, true); // No absolute vault path on first-setup response (filesystem paths stay off unauth JSON). // Auth'd admin_login may still return the operator path hint for controlpanel. api_out([ 'ok' => true, 'msg' => 'hash in data/admin.pass.hash; clear password also written to vault/ (operator only); password unlocks panel only — faucet is treasury seed', ]); } if ($api === 'admin_login' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); $e = load_economy(); $mid = book_mid_micros($chain); api_out([ 'ok' => true, 'msg' => 'ok', 'epoch' => epoch_info(), 'mods_count' => count(load_mods()), 'treasury_addr' => $taddr, 'treasury_bal_fmt' => fmt_amt($bal[$taddr] ?? '0'), 'chain_len' => count($chain), 'vault_hint' => vault_dir(), 'site' => 'trade', 'version' => NST_VERSION, 'economy' => [ 'profit_fmt' => fmt_amt((string)($e['profit_micros'] ?? '0')), 'ad_bid_fmt' => fmt_amt((string)($e['ad_bid_micros'] ?? '0')), 'donate_k' => (string)($e['donate_k'] ?? NST_DONATE_K), 'value_note' => (string)($e['value_note'] ?? ''), 'value_authority' => 'this_server_owner', 'mirrors_role' => 'free_tributaries', 'mid_fmt' => $mid !== null ? fmt_amt($mid) : null, 'bids_n' => is_array($e['bids'] ?? null) ? count($e['bids']) : 0, ], 'donate_addrs' => [ 'btc' => NST_DONATE_BTC, 'xmr' => NST_DONATE_XMR, 'ltc' => NST_DONATE_LTC, ], ]); } /** Renter sets local NSU value policy (k + public note). Not a global peg. Mirrors do not inherit. */ if ($api === 'admin_set_value' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); $e = load_economy(); $k = trim((string)($_POST['donate_k'] ?? '')); if ($k !== '') { if (!preg_match('/^\d+(\.\d{1,8})?$/', $k) || (float)$k <= 0) { api_out(['ok' => false, 'err' => 'donate_k must be positive number'], 400); } $e['donate_k'] = $k; } if (array_key_exists('value_note', $_POST)) { $note = trim((string)$_POST['value_note']); if (strlen($note) > 240) $note = substr($note, 0, 240); $e['value_note'] = $note; } save_economy($e); api_out([ 'ok' => true, 'msg' => 'local value policy saved - this server owner only; mirrors stay free tributaries', 'donate_k' => (string)($e['donate_k'] ?? NST_DONATE_K), 'value_note' => (string)($e['value_note'] ?? ''), 'value_authority' => 'this_server_owner', 'mirrors_role' => 'free_tributaries', ]); } if ($api === 'admin_change_pass' && $_SERVER['REQUEST_METHOD'] === 'POST') { // KING-only rotate: LORD cannot re-key a password door (even though faucet is seed-only). require_faucet_auth(); $new = (string)($_POST['new_pass'] ?? ''); admin_pass_set($new, true); api_out(['ok' => true, 'msg' => 'password rotated; vault updated if writable; password is panel migrate only — not faucet']); } if ($api === 'admin_reveal' && $_SERVER['REQUEST_METHOD'] === 'POST') { // KING / legacy only — LORD must never receive treasury.secret (CONTRACT §3). require_faucet_auth(); global $TFILE, $MODS_FILE, $META, $CHAIN, $ADMIN_HASH_FILE; api_out([ 'ok' => true, 'admin_pass' => null, // cleartext only in vault/ for root pull 'admin_pass_note' => 'Legacy clear password may be in vault/ADMIN_PASSWORD.txt (root pull). Product door is site seed.', 'treasury_secret' => treasury_secret(), 'treasury_addr' => $taddr, 'vault_dir' => vault_dir(), 'paths' => [ 'admin_hash' => basename($ADMIN_HASH_FILE), 'treasury_secret' => basename($TFILE), 'modifiers' => basename($MODS_FILE), 'meta' => basename($META), 'chain' => basename($CHAIN), ], 'note' => 'KING faucet material. Renters never receive this. OS root stays SSH. Host compromise = game over for this crop.', ]); } // Renter utter control: read/replace THIS index.php (app god-mode, not OS root) if ($api === 'admin_get_source' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); $raw = (string)file_get_contents(__FILE__); api_out([ 'ok' => true, 'bytes' => strlen($raw), 'sha256' => hash('sha256', $raw), 'source' => $raw, 'warning' => 'Saving a broken file can kill this site until SSH restore.', ]); } if ($api === 'admin_put_source' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); require_admin(); $src = (string)($_POST['source'] ?? ''); if (strlen($src) < 100) api_out(['ok' => false, 'err' => 'source too short'], 400); if (strpos($src, ' false, 'err' => 'must look like PHP'], 400); $bak = __FILE__ . '.bak.' . time(); @copy(__FILE__, $bak); if (file_put_contents(__FILE__, $src, LOCK_EX) === false) { api_out(['ok' => false, 'err' => 'write failed - check perms'], 500); } api_out([ 'ok' => true, 'msg' => 'index.php replaced', 'backup' => basename($bak), 'sha256' => hash('sha256', $src), 'bytes' => strlen($src), ]); } if ($api === 'admin_pay' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); // Free-form treasury pay = KING faucet authority (not LORD panel seed alone). require_faucet_auth(); $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['to'] ?? ''))); $micros = parse_amt(trim((string)($_POST['amount'] ?? ''))); $memo = substr(trim((string)($_POST['memo'] ?? 'admin:pay')), 0, 120); if (strlen($to) !== 64) api_out(['ok' => false, 'err' => 'bad to'], 400); if ($micros === null || dec_cmp($micros, '1') < 0) api_out(['ok' => false, 'err' => 'bad amount'], 400); $sec = treasury_secret(); $from = addr_from_seed($sec); $ts = time(); $nonce = bin2hex(random_bytes(8)); $body = implode('|', ['transfer', '1', $from, $to, $micros, $nonce, (string)$ts, $memo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $to, 'amount' => $micros, 'amount_fmt' => fmt_amt($micros), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => mac_sign($sec, $body), 'kind' => 'admin_pay', ]; $payOut = nst_with_chain_lock(function (array $chain, callable $append) use ($from, $to, $micros, $nonce, $row) { foreach ($chain as $r) { if (($r['type'] ?? '') === 'transfer' && ($r['from'] ?? '') === $from && ($r['nonce'] ?? '') === $nonce) { return ['ok' => false, 'http' => 400, 'err' => 'nonce replay']; } } $bal = balances($chain); $have = $bal[$from] ?? '0'; if (dec_cmp($have, $micros) < 0) { return ['ok' => false, 'http' => 400, 'err' => 'treasury insufficient', 'have_fmt' => fmt_amt($have)]; } if (!$append($row)) { return ['ok' => false, 'http' => 500, 'err' => 'disk']; } return ['ok' => true, 'tx' => $row, 'treasury_bal_fmt' => fmt_amt(dec_sub($have, $micros))]; }); if ($payOut === null) { api_out(['ok' => false, 'err' => 'disk'], 500); } if (empty($payOut['ok'])) { $http = (int)($payOut['http'] ?? 400); unset($payOut['http']); api_out($payOut, $http); } api_out($payOut); } /** * OPERATOR_GO §4 / E2a: KING seeds real fillable SELL-ask ladder on the book. * Not free giveaway — visitors BUY by filling these asks (NOTE settle_ref off-chain). * Each treasury SELL auto-arms C2 half-BID. Lords cannot call (require_faucet_auth). */ if ($api === 'admin_treasury_ladder' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); require_faucet_auth(); // Pre-read treasury material OUTSIDE chain lock (deadlock law; same as C2 half-bid). $tsec = treasury_secret(); $taddr = strtolower(addr_from_seed($tsec)); $amtIn = trim((string)($_POST['amount'] ?? '')); $amtMicros = null; if ($amtIn !== '') { $amtMicros = parse_amt($amtIn); if ($amtMicros === null || dec_cmp($amtMicros, '1') < 0) { api_out(['ok' => false, 'err' => 'bad amount'], 400); } } $ladder = nst_treasury_ask_ladder($tsec, $taddr, $amtMicros); if (empty($ladder['ok'])) { $http = 400; $err = (string)($ladder['err'] ?? ''); if ($err === 'disk' || str_contains($err, 'half-bid failed')) { $http = 500; } api_out($ladder, $http); } // Ladder half-BID notices: AFTER money/book writes, fail-open. try { $hbList = is_array($ladder['half_bids'] ?? null) ? $ladder['half_bids'] : []; $ordList = is_array($ladder['orders'] ?? null) ? $ladder['orders'] : []; foreach ($hbList as $i => $hb) { if (!is_array($hb)) continue; $sell = is_array($ordList[$i] ?? null) ? $ordList[$i] : null; nst_inbox_notify_half_bid($sell, $hb); } } catch (Throwable $e) { // never abort a committed ladder } api_out($ladder); } /** Credit a donate: gift NSU to addr (mid×k or direct), 50/50 profit/ad-bid accounting */ if ($api === 'admin_donate_credit' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); // Donate gifts pull from treasury faucet — KING only (CONTRACT §3). require_faucet_auth(); $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['to'] ?? ''))); if (strlen($to) !== 64) api_out(['ok' => false, 'err' => 'bad to'], 400); $txid = substr(trim((string)($_POST['txid'] ?? '')), 0, 128); $asset = strtoupper(substr(trim((string)($_POST['asset'] ?? 'BTC')), 0, 16)); $ext = trim((string)($_POST['ext_amount'] ?? '')); $direct = trim((string)($_POST['nsu_amount'] ?? '')); $e = load_economy(); $k = (string)($e['donate_k'] ?? NST_DONATE_K); $mid = book_mid_micros(read_chain()); if ($direct !== '') { $nsu = parse_amt($direct); } else { $nsu = donate_nsu_from_external($ext !== '' ? $ext : '0', $mid, $k); } if ($nsu === null || dec_cmp($nsu, '1') < 0) { api_out(['ok' => false, 'err' => 'bad amount', 'mid_fmt' => $mid !== null ? fmt_amt($mid) : null, 'k' => $k], 400); } $r = economy_credit_donate($to, $nsu, $txid, $asset, $ext !== '' ? $ext : $direct); if (empty($r['ok'])) api_out($r, isset($r['err']) && str_contains((string)$r['err'], 'treasury') ? 503 : 400); $r['mid_fmt'] = $mid !== null ? fmt_amt($mid) : null; $r['k'] = $k; api_out($r); } /** Extract profit pile → pay operator addr from treasury; reduce profit accounting */ if ($api === 'admin_extract_profit' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); // Treasury debit = faucet authority (KING only). LORD spends funded wallet, never treasury.secret. require_faucet_auth(); $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['to'] ?? ''))); $micros = parse_amt(trim((string)($_POST['amount'] ?? ''))); if (strlen($to) !== 64) api_out(['ok' => false, 'err' => 'bad to'], 400); if ($micros === null || dec_cmp($micros, '1') < 0) api_out(['ok' => false, 'err' => 'bad amount'], 400); $sec = treasury_secret(); $from = addr_from_seed($sec); $ts = time(); $nonce = bin2hex(random_bytes(8)); $memo = 'profit:extract'; $body = implode('|', ['transfer', '1', $from, $to, $micros, $nonce, (string)$ts, $memo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $to, 'amount' => $micros, 'amount_fmt' => fmt_amt($micros), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => mac_sign($sec, $body), 'kind' => 'profit_extract', ]; // Chain lock covers treasury debit; re-load economy under lock so pile cannot double-extract. $exOut = nst_with_chain_lock(function (array $chain, callable $append) use ($from, $to, $micros, $nonce, $row) { $e = load_economy(); $pile = (string)($e['profit_micros'] ?? '0'); if (dec_cmp($pile, $micros) < 0) { return ['ok' => false, 'http' => 400, 'err' => 'profit pile too small', 'have_fmt' => fmt_amt($pile)]; } foreach ($chain as $r) { if (($r['type'] ?? '') === 'transfer' && ($r['from'] ?? '') === $from && ($r['nonce'] ?? '') === $nonce) { return ['ok' => false, 'http' => 400, 'err' => 'nonce replay']; } } $bal = balances($chain); $have = $bal[$from] ?? '0'; if (dec_cmp($have, $micros) < 0) { return ['ok' => false, 'http' => 400, 'err' => 'treasury insufficient', 'have_fmt' => fmt_amt($have)]; } if (!$append($row)) { return ['ok' => false, 'http' => 500, 'err' => 'disk']; } $e['profit_micros'] = dec_sub($pile, $micros); save_economy($e); return [ 'ok' => true, 'tx' => $row, 'profit_fmt' => fmt_amt((string)$e['profit_micros']), 'treasury_bal_fmt' => fmt_amt(dec_sub($have, $micros)), ]; }); if ($exOut === null) { api_out(['ok' => false, 'err' => 'disk'], 500); } if (empty($exOut['ok'])) { $http = (int)($exOut['http'] ?? 400); unset($exOut['http']); api_out($exOut, $http); } api_out($exOut); } if ($api === 'economy') { $e = load_economy(); $mid = book_mid_micros(read_chain()); $active = array_values(array_filter($e['bids'] ?? [], fn($b) => !empty($b['active']))); $withCreative = 0; foreach ($active as $b) { if (!empty($b['creative_url']) || !empty($b['creative_file'])) $withCreative++; } api_out([ 'ok' => true, 'profit_fmt' => fmt_amt((string)($e['profit_micros'] ?? '0')), 'ad_bid_fmt' => fmt_amt((string)($e['ad_bid_micros'] ?? '0')), 'donate_k' => (string)($e['donate_k'] ?? NST_DONATE_K), 'mid_fmt' => $mid !== null ? fmt_amt($mid) : null, 'epoch_n' => (int)(load_meta()['epoch_n'] ?? 1), 'donate_addrs' => [ 'btc' => NST_DONATE_BTC, 'xmr' => NST_DONATE_XMR, 'ltc' => NST_DONATE_LTC, ], 'active_bids' => $active, 'bids_with_creative' => $withCreative, ]); } /** Public catalogue of in-file purpose brains (no secrets; ethos visibility). */ if ($api === 'brains') { api_out([ 'ok' => true, 'site' => 'trade', 'version' => NST_VERSION, 'doctrine' => 'tiny in-file specialists only; no external LLM APIs; fail open when unsure', 'brains' => [ ['id' => 'memo_risk', 'job' => 'scam tokens in transfer memos', 'mode' => 'soft flag'], ['id' => 'order_sanity', 'job' => 'side/amount/price sanity', 'mode' => 'soft flag'], ['id' => 'pair_sane', 'job' => 'book pair format NSU/NOTE…', 'mode' => 'soft flag'], ['id' => 'wipe_copy', 'job' => 'balances-only year wipe banner copy', 'mode' => 'coherence'], ['id' => 'ad_png', 'job' => 'PNG magic+IHDR geometry + byte budget', 'mode' => 'hard reject + soft flag'], ['id' => 'href_risk', 'job' => 'http(s) click-through schemes', 'mode' => 'hard scheme + soft phishing'], ['id' => 'year_wipe_rekey', 'job' => 'rotate panel password into vault on epoch roll', 'mode' => 'automatic'], ['id' => 'house_ads', 'job' => 'weighted house discovery when empty + luck slice', 'mode' => 'allotted random'], ['id' => 'bootstrap_spread', 'job' => 'loose BUY@0.5/SELL@1.5 around 1.0 if zero open orders', 'mode' => 'seed discovery only'], ['id' => 'panel_seed', 'job' => 'controlpanel unlock via site wallet seed (treasury)', 'mode' => 'no recovery'], ], 'contact' => NST_CONTACT_EMAIL, 'ad_png_rules' => [ 'max_bytes' => NST_AD_MAX_PNG, 'min_w' => NST_AD_MIN_W, 'max_w' => NST_AD_MAX_W, 'min_h' => NST_AD_MIN_H, 'max_h' => NST_AD_MAX_H, 'formats' => ['PNG only'], ], 'wipe' => 'ONLY user wallet balances survive yearly wipe; profit/ads/orders burn', ]); } /** Weighted random creative for site-local adboard (paid bids + house discovery) */ if ($api === 'ad_pick') { $b = ad_pick_weighted(); if (!$b) { api_out(['ok' => true, 'empty' => true, 'msg' => 'no creatives']); } $house = !empty($b['house']); $img = null; if (!$house && !empty($b['creative_url'])) { $img = ['type' => 'url', 'src' => $b['creative_url']]; } elseif (!$house && !empty($b['creative_file'])) { $img = ['type' => 'api', 'src' => '?api=ad_img&id=' . urlencode((string)$b['id']) . '&v=' . (int)($b['creative_ts'] ?? 0)]; } $pngBrain = $b['png_brain'] ?? null; api_out([ 'ok' => true, 'empty' => false, 'house' => $house, 'label' => $b['label'] ?? '', 'bid_id' => $b['id'] ?? '', 'stake_fmt' => $house ? 'house' : ($b['stake_fmt'] ?? fmt_amt((string)($b['stake_micros'] ?? '0'))), 'img' => $img, 'href' => $b['href'] ?? '', 'png_w' => $b['png_w'] ?? ($pngBrain['w'] ?? null), 'png_h' => $b['png_h'] ?? ($pngBrain['h'] ?? null), 'png_bytes' => $b['png_bytes'] ?? ($pngBrain['bytes'] ?? null), 'png_flag' => !empty($pngBrain['flag']), 'png_notes' => $pngBrain['notes'] ?? [], 'recruit' => NST_CONTACT_EMAIL, ]); } if ($api === 'ad_img') { $id = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_GET['id'] ?? ''))); if (strlen($id) < 16) { http_response_code(404); exit; } $path = ads_dir() . DIRECTORY_SEPARATOR . $id . '.png'; if (!is_file($path)) { http_response_code(404); exit; } header('Content-Type: image/png'); header('Cache-Control: public, max-age=300'); readfile($path); exit; } /** Attach creative to a bid (URL and/or PNG base64). Site-local board only. */ if ($api === 'admin_ad_creative' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); require_admin(); $id = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['bid_id'] ?? ''))); $url = trim((string)($_POST['creative_url'] ?? '')); $href = trim((string)($_POST['href'] ?? '')); $b64 = (string)($_POST['png_base64'] ?? ''); if (strlen($id) < 16) api_out(['ok' => false, 'err' => 'bad bid_id'], 400); $brains = []; if ($href !== '') { $hrefBrain = nst_brain_href_risk($href); $brains['href_risk'] = $hrefBrain; if (empty($hrefBrain['ok'])) { api_out(['ok' => false, 'err' => (string)($hrefBrain['err'] ?? 'bad href'), 'brains' => $brains], 400); } } if ($url !== '') { $urlBrain = nst_brain_href_risk($url); $brains['creative_url'] = $urlBrain; if (empty($urlBrain['ok'])) { api_out(['ok' => false, 'err' => (string)($urlBrain['err'] ?? 'bad creative_url'), 'brains' => $brains], 400); } } $pngMeta = null; $e = load_economy(); $found = false; foreach ($e['bids'] as &$b) { if (($b['id'] ?? '') !== $id) continue; $found = true; if ($url !== '') { $b['creative_url'] = substr($url, 0, 500); } if ($href !== '') { $b['href'] = substr($href, 0, 500); if (!empty($brains['href_risk']['flag'])) { $b['href_flag'] = $brains['href_risk']; } } if ($b64 !== '') { if (str_starts_with($b64, 'data:image/png;base64,')) { $b64 = substr($b64, strlen('data:image/png;base64,')); } elseif (str_starts_with($b64, 'data:image/')) { api_out(['ok' => false, 'err' => 'PNG only (no JPEG/GIF/WebP data URLs)', 'brains' => $brains], 400); } $bin = base64_decode($b64, true); if ($bin === false) { api_out(['ok' => false, 'err' => 'bad base64', 'brains' => $brains], 400); } $pngBrain = nst_brain_ad_png($bin); $brains['ad_png'] = $pngBrain; if (empty($pngBrain['ok'])) { api_out(['ok' => false, 'err' => (string)($pngBrain['err'] ?? 'png reject'), 'brains' => $brains], 400); } $dir = ads_dir(); $fp = $dir . DIRECTORY_SEPARATOR . $id . '.png'; if (file_put_contents($fp, $bin, LOCK_EX) === false) { api_out(['ok' => false, 'err' => 'disk ad', 'brains' => $brains], 500); } @chmod($fp, 0644); $b['creative_file'] = $id . '.png'; $b['png_w'] = $pngBrain['w'] ?? null; $b['png_h'] = $pngBrain['h'] ?? null; $b['png_bytes'] = $pngBrain['bytes'] ?? strlen($bin); $b['png_brain'] = $pngBrain; $pngMeta = $pngBrain; // prefer local file over url when both set } $b['creative_ts'] = time(); break; } unset($b); if (!$found) api_out(['ok' => false, 'err' => 'bid not found', 'brains' => $brains], 404); save_economy($e); $msg = 'creative set'; if ($pngMeta && !empty($pngMeta['flag'])) { $msg .= ' (soft flag: ' . implode(',', $pngMeta['notes'] ?? []) . ')'; } api_out(['ok' => true, 'msg' => $msg, 'bid_id' => $id, 'png' => $pngMeta, 'brains' => $brains]); } if ($api === 'admin_backup' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); $wantSecrets = !empty($_POST['include_secrets']); if ($wantSecrets) { // treasury.secret in export is KING-only (CONTRACT §3). require_faucet_auth(); } $chain = read_chain(); $mods = load_mods(); $meta = load_meta(); api_out([ 'ok' => true, 'version' => NST_VERSION, 'exported_ts' => time(), 'meta' => $meta, 'epoch' => epoch_info(), 'mods' => $mods, 'chain_len' => count($chain), 'chain' => $chain, 'treasury_addr' => $taddr, 'include_secrets' => $wantSecrets, 'treasury_secret' => $wantSecrets ? treasury_secret() : null, 'admin_pass' => null, 'admin_pass_note' => 'product door is site seed; legacy clear password only in vault/ if ever set', ]); } /** * Public rent quote (P3a+P3b / NSU_VIABILITY E3 quote half). * GET or POST ?api=rent_quote&site=trade|com|… — no auth, no chain write, no bind. * Persists open quote to data/rent_quotes.json under flock (P3b). */ if ($api === 'rent_quote') { $siteQ = (string)($_GET['site'] ?? $_POST['site'] ?? $_REQUEST['site'] ?? 'trade'); $q = nst_rent_quote_public($siteQ, $taddr); $http = (int)($q['http'] ?? 200); unset($q['http']); api_out($q, $http > 0 ? $http : 200); } /** * Lookup durable quote (P3b). Open or paid. * GET ?api=rent_quote_get"e_id= */ if ($api === 'rent_quote_get') { $qid = (string)($_GET['quote_id'] ?? $_POST['quote_id'] ?? $_REQUEST['quote_id'] ?? ''); $row = nst_rent_quote_get($qid); if ($row === null) { api_out(['ok' => false, 'err' => 'quote not found or expired', 'settle' => false], 404); } api_out([ 'ok' => true, 'quote' => $row, 'durable' => true, 'paid' => ((string)($row['status'] ?? '') === 'paid'), 'law' => 'Lookup only — pay via transfer to treasury with exact memo+amount.', ]); } /** * P3c: settle rent from chain evidence (scan transfers). Optional quote_id filter. * GET|POST ?api=rent_settle"e_id= — no auth; first-paid-wins already on transfer path. */ if ($api === 'rent_settle') { $qidS = (string)($_GET['quote_id'] ?? $_POST['quote_id'] ?? $_REQUEST['quote_id'] ?? ''); $scan = nst_rent_settle_scan($qidS !== '' ? $qidS : null); api_out($scan, !empty($scan['settled']) ? 200 : 200); } /** Rent grant path retired — always 410 after KING auth. No treasury credit. */ if ($api === 'admin_rent_claim' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_faucet_auth(); api_out([ 'ok' => false, 'err' => 'rent grant path retired', 'law' => 'Rent is payment TO treasury only (not a grant FROM faucet). ' . 'Server no longer spawns LORD seeds or credits renters from treasury. ' . 'Public quotes: ?api=rent_quote&site=… (durable) · settle/bind still next residual.', 'lord_rent_grant' => 'retired', 'next' => 'rent_quote durable (live) · pay-bind settle (not shipped)', ], 410); } api_out(['ok' => false, 'err' => 'unknown api'], 404); } /* -------- /controlpanel — site wallet seed unlock (no recovery) -------- */ ensure_genesis(); $wantPanel = isset($_GET['controlpanel']) || (isset($_SERVER['REQUEST_URI']) && preg_match('#/controlpanel/?(\?|$)#', (string)$_SERVER['REQUEST_URI'])); if ($wantPanel && (string)($_GET['api'] ?? $_POST['api'] ?? '') === '') { header('Content-Type: text/html; charset=UTF-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); $v = NST_VERSION; $panelSeed = ''; $unlocked = false; $loginErr = ''; // POST seed → derive addr; unlock if treasury (owner) or economy.operator_addr (rent later) if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') { $panelSeed = norm_seed((string)($_POST['seed'] ?? '')); if ($panelSeed === '') { $loginErr = 'Paste this site\'s wallet seed (12 words). No recovery.'; } elseif (!panel_seed_ok($panelSeed)) { $loginErr = 'Seed does not unlock this site panel (must match site wallet / treasury).'; $panelSeed = ''; } else { $unlocked = true; } } $esc = static function (string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }; echo ''; echo 'Trade Control Panel
'; echo '

DNA · TRADE · NSU rails · orange burn

'; echo '

Nosignup.Trade · Control Panel

'; echo '

Paste this site\'s operator seed (no recovery). Fresh genesis: panel = data/site.seed (not mint); KING faucet = data/treasury.secret (only minter). ' . 'After public rent pay (visitor pays treasury exact quote), payer key becomes LORD for the epoch — no server-spawned LORD seed. ' . 'UTTER control of THIS crop (including replacing this file). Not OS root. ' . 'Genesis supply lives in treasury (24_000_000 NSU KING faucet). ' . 'Renters must not receive treasury secret. Yearly wipe: ONLY user balances survive; book re-seeds loose bootstrap spread; superstructure burns.

'; echo '
'; echo ''; echo ''; echo ''; if ($loginErr !== '') { echo '
' . $esc($loginErr) . '
'; } echo '

Panel: data/site.seed + vault SITE_WALLET_SEED.txt. KING (mint only): data/treasury.secret + vault KING_FAUCET_SEED.txt. After rent: LORD = payer key (economy.operator_addr). No recovery desk. No password product path.

'; echo '
Version-
'; echo '
Treasury (master faucet)-
'; echo '
Profit pile-
'; echo '
Ad bid stake-
'; echo '
Book mid / k-
'; echo '
Value note-
'; echo '
Chain / epoch-
'; echo '
Vault hint-
'; echo '

'; echo '

NSU value (this server owner only)

'; echo '

Value authority = you (owner of THIS crop). Set donate k (gift ≈ external÷mid×k) and a public value note. Mirrors are free tributaries — no value authority. Market P = book mid; donate buyback arms at P/2.

'; echo ''; echo '
'; echo '

Year-end wipe checklist (operator)

'; echo '

ONLY wallet balances survive. Profit/ads/orders burn. Site wallet seed does NOT rotate (still your paper). Bootstrap book re-seeds if empty. Tick boxes local only.

'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo ''; echo '

Rent / LORD (grant path retired · quote live)

'; echo '

RETIRED: treasury→LORD market grant + server-spawned LORD seed are gone (atomic P2). ' . 'P3a–c live: ?api=rent_quote · durable store · pay treasury with exact memo+amount · ' . 'first-paid-wins binds operator_addr to payer key (no server LORD seed). ' . 'Lookup rent_quote_get · rescan rent_settle. ' . 'Failed rent pays (wrong amount / already paid / expired / …) auto-refund to payer (P3d). ' . 'Faucet start narrative still ' . (int)NST_KING_FAUCET_START_NSU . ' NSU. Renters never mint. ' . 'Never email treasury.secret.

'; echo ''; echo '
Grant path retired · try ?api=rent_quote&site=trade (no settle yet)
'; echo ''; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo ''; echo '

Credit donate → NSU gift (50/50 split)

'; echo '

Gifts NSU from treasury master faucet (nsu_amount OR ext_amount÷mid×k), then 50% profit / 50% ad-bid + buyback at P/2. Free faucet claims do not use this path.

'; echo ''; echo ''; echo '
'; echo '

Extract profit

'; echo '
'; echo '

Ad creative (site-local weighted board)

'; echo '

PNG only. Max ' . NST_AD_MAX_PNG . 'b · ' . NST_AD_MIN_W . '-' . NST_AD_MAX_W . '×' . NST_AD_MIN_H . '-' . NST_AD_MAX_H . ' px. House discovery fills empty boards. Invite: ' . $esc(NST_CONTACT_EMAIL) . '

'; echo ''; echo ''; echo '
'; echo '

Treasury pay (master faucet)

'; echo '
'; echo '

Source (utter control)

Load → edit → save replaces this site file. Broken save can kill the site until SSH restore from .bak.

'; echo ''; echo '
'; echo '

Sealed wallet inbox (KING · experimental)

'; echo '

Mail ≠ mint. KING hand-seal = true client-encrypt E2E path. Auto fill/half-BID/rent notices = server-composed public facts (encrypted for delivery, NOT private E2E). ' . 'Server stores opaque ciphertext only — paste plaintext only in this browser; encryption runs client-side when WebCrypto works. ' . 'Not social mail. Not proven e2e. Blobs burn at yearly wipe / TTL.

'; echo ''; echo '
'; echo '

Backup

'; echo '

← back to trade

'; // Seed stays in memory for API auth only after successful POST unlock (not echoed into form). echo ''; exit; } /* -------- HTML -------- */ // ensure_genesis already called for panel path; safe again ensure_genesis(); $chainSsr = read_chain(); $taddr = treasury_addr(); $tbal = fmt_amt(balances($chainSsr)[$taddr] ?? '0'); // E2 conservation boot (HTML): honor freeze; recheck if stale $nstConserveSnap = nst_conservation_boot_check($chainSsr, false); $nstConservePub = nst_conservation_public_view($nstConserveSnap); $nstFrozen = !empty($nstConservePub['frozen']); // SSR market P + buyback P/2 (same helpers as ?api=state) so strip is honest before JS $ssrP = book_mid_micros($chainSsr); $ssrP2 = nst_buyback_price_micros($chainSsr); $ssrP_fmt = $ssrP !== null ? fmt_amt($ssrP) : '-'; $ssrP2_fmt = $ssrP2 !== null ? fmt_amt($ssrP2) : '-'; $ssrBoot = false; foreach (open_orders($chainSsr) as $oSsr) { if (($oSsr['kind'] ?? '') === 'bootstrap_spread') { $ssrBoot = true; break; } } $selfHash = @hash_file('sha256', __FILE__) ?: 'unavailable'; header('Content-Type: text/html; charset=UTF-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); ?> Nosignup.Trade >
Nosignup.Trade free disposable wallet · no signup
P RESET IN d
LOCKED
site adboard · house discovery when empty · 15–30s rotate

Free/no-signup PNG house ads → · no account · this crop only

Operator console Ctrl+Alt+Shift+N · site seed

Unlock with site wallet seed

Product door is the same 12-word site seed as /controlpanel (U7). No recovery desk. Preferred surface: /controlpanel. Host root compromise = game over — accepted.

Legacy password migrate (one-release debt only)

Only if data/admin.pass.hash already exists from an older deploy. Not the product path. New installs use seed only.