invent):
* - Home: FOUR sector buttons (Free / Items / Real Estate / Vehicles) + one search bar.
* - Button click → post-ad modal for that sector (existing ?api=post). Search → flood ranked listings
* (existing ?api=list with multi-token score). No corner browse panels.
* - Work-family buttons: start ~50%x50% quarters, shrink to 10%x10%. Future ad countdown on buttons
* is design-compatible only (not implemented). DNA: MARKET · classified gold.
* - Empire entrance (chat DNA): Options · Mirrors · Donate + ENTER — never corner DBG/FAB primary.
*/
/**
* FILE MAP (cells/market.php — physical order; comments only, not a second spec):
* charter + MARKET CONTRACT .... law / keep-working / never-become (above)
* vault / panel / admin ........ nsp_vault_* · nsp_handle_admin_api · nsp_render_controlpanel
* constants + brains ........... NSM_* · nsm_brain_listing_spam · nsm_brain_contact_risk
* storage helpers .............. listings.jsonl (durable) · rate via nsm_ephemeral_dir · append-safe LOCK_EX compact · TTL
* ?src=1 / ?download ........... auditable source stream
* public API + panel dispatch .. ?api=state|list|post|edit|delete · admin_* · controlpanel
* main HTML/CSS ................ hyper-simple shell · work-family sectors · search flood · entry chrome
* home + entry DOM ............. four sector post buttons · flood list · nsGate · Options/Mirrors/Donate
* client JS (sectors + entry) .. post modal · floodListings rank · entry boot
* Edit this cell only; sync-pack writes root/pack 7.php. Do not hand-edit pack.
*/
declare(strict_types=1);
if (preg_match('/^www\.(.+)$/i', preg_replace('/:\d+$/', '', (string)($_SERVER['HTTP_HOST'] ?? '')), $m)) { header('Location: //' . strtolower($m[1]) . (string)($_SERVER['REQUEST_URI'] ?? '/'), true, 301); exit; }
/**
* LEGACY flat rent placeholder only — NEVER implement / wire / mint on market.
* Trade retired flat 10k as LORD_RENT_CREDIT_LEGACY; live LORD grants are
* market-value formula on nosignup.trade (KING faucet → LORD), not a local const.
* Market has no visitor ledger, no site-local treasury, no rent_claim, no mint.
* Money/buy on nosignup.trade. Do not re-introduce live LORD_RENT_CREDIT.
*/
const LORD_RENT_CREDIT_LEGACY = 10000; // retired flat placeholder (whole unit) — never mint here
/* ---- SITE-LOCAL CONTROL PANEL (renter key; not OS root) ---- */
function nsp_vault_dir(): string {
$sib = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vault';
$loc = __DIR__ . DIRECTORY_SEPARATOR . 'vault';
foreach ([$sib, $loc] as $d) {
if (is_dir($d) || @mkdir($d, 0700, true)) {
if (is_dir($d) && is_writable($d)) return $d;
}
}
return $sib;
}
function nsp_data_dir(): string {
$d = __DIR__ . DIRECTORY_SEPARATOR . 'data';
if (!is_dir($d)) @mkdir($d, 0755, true);
$ht = $d . DIRECTORY_SEPARATOR . '.htaccess';
if (!is_file($ht)) @file_put_contents($ht, "Require all denied\nDeny from all\n");
return $d;
}
function nsp_hash_file(): string { return nsp_data_dir() . DIRECTORY_SEPARATOR . 'admin.pass.hash'; }
function nsp_seed_file(): string { return nsp_data_dir() . DIRECTORY_SEPARATOR . 'site.seed'; }
function nsp_norm_seed(string $s): string {
return strtolower(trim(preg_replace('/\s+/', ' ', $s) ?? ''));
}
/** Identity surface only (same scheme as trade); market has no visitor spend ledger. */
function nsp_addr_from_seed(string $seed): string {
return hash('sha256', 'nsu-addr-v1|' . nsp_norm_seed($seed));
}
/**
* 12-word site seed (not BIP39). CSPRNG into fixed word pool.
* Panel unlock for THIS crop only — not a faucet mint (market has no visitor wallet).
*/
function nsp_generate_site_seed(): string {
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);
}
/** Owner-only vault note: site seed = panel unlock for THIS crop. Never to renters/visitors. */
function nsp_vault_site_seed_note(string $seed): void {
$d = nsp_vault_dir();
if (!is_dir($d) && !@mkdir($d, 0700, true)) {
return;
}
@chmod($d, 0700);
$body = "NOSIGNUP.MARKET SITE WALLET SEED (OWNER ONLY)\n"
. "This seed unlocks /controlpanel for THIS crop only.\n"
. "Market has no visitor ledger or mint on this crop. Money/buy: nosignup.trade.\n"
. "NO RECOVERY. Renters must NOT receive this secret (LORD seed is issued offline per epoch).\n"
. "Generated: " . gmdate('c') . "\n\n"
. trim($seed) . "\n";
@file_put_contents($d . DIRECTORY_SEPARATOR . 'SITE_WALLET_SEED.txt', $body, LOCK_EX);
@chmod($d . DIRECTORY_SEPARATOR . 'SITE_WALLET_SEED.txt', 0600);
}
/** True if $seed matches data/site.seed (normalized). */
function nsp_panel_seed_ok(string $seed): bool {
$seed = nsp_norm_seed($seed);
if ($seed === '') {
return false;
}
$path = nsp_seed_file();
if (!is_file($path)) {
return false;
}
$have = nsp_norm_seed((string)@file_get_contents($path));
if ($have === '') {
return false;
}
return hash_equals($have, $seed);
}
/**
* Ensure data/site.seed exists; mirror to vault SITE_WALLET_SEED.txt on first write.
* Idempotent. Call before admin API so setup is never land-grabable.
* NOT a treasury faucet mint — market has no visitor ledger this crop.
*/
function nsp_ensure_site_seed(): void {
nsp_pass_burn();
$path = nsp_seed_file();
if (is_file($path) && nsp_norm_seed((string)@file_get_contents($path)) !== '') {
$vd = nsp_vault_dir();
$note = $vd . DIRECTORY_SEPARATOR . 'SITE_WALLET_SEED.txt';
if (!is_file($note) || trim((string)@file_get_contents($note)) === '') {
nsp_vault_site_seed_note(nsp_norm_seed((string)@file_get_contents($path)));
}
return;
}
$seed = nsp_generate_site_seed();
file_put_contents($path, $seed . "\n", LOCK_EX);
@chmod($path, 0600);
nsp_vault_site_seed_note($seed);
}
function nsp_json(array $x, int $c = 200): void {
http_response_code($c);
header('Content-Type: application/json; charset=UTF-8');
header('Cache-Control: no-store');
echo json_encode($x, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
function nsp_pass_burn(): void {
$paths = [
nsp_hash_file(),
nsp_data_dir() . DIRECTORY_SEPARATOR . 'admin.pass.txt',
nsp_vault_dir() . DIRECTORY_SEPARATOR . 'ADMIN_PASSWORD.txt',
];
foreach ($paths as $p) {
if (is_string($p) && $p !== '' && is_file($p) && !is_link($p)) {
@unlink($p);
}
}
}
function nsp_require(): void {
// POST body only — never accept seed from query (URL/access logs/Referer).
nsp_pass_burn();
$seed = (string)($_POST['seed'] ?? '');
if ($seed !== '' && nsp_panel_seed_ok($seed)) {
return;
}
nsp_json(['ok' => false, 'err' => 'admin auth'], 401);
}
function nsp_handle_admin_api(string $api): bool {
if (!str_starts_with($api, 'admin_')) return false;
nsp_ensure_site_seed();
if ($api === 'admin_status') {
// Soft-verify: site + version only. No filesystem vault path to strangers.
nsp_json(['ok' => true, 'site' => 'market', 'version' => NSM_VERSION]);
}
if ($api === 'admin_login' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
nsp_require();
nsp_json(['ok' => true, 'msg' => 'ok', 'site' => 'market', 'vault_hint' => nsp_vault_dir(), 'version' => NSM_VERSION]);
}
/* LORD SOVEREIGNTY — rotate this crop's panel seed.
*
* Until this existed a lord could not become independent of the king. The
* ten panel seeds are minted by genesis and every one of them is printed in
* the king's GENESIS-INFO.txt, so the key to a rented crop was issued by the
* landlord and the landlord kept a copy. admin_change_pass rotates only the
* legacy PASSWORD, which changes nothing: nsp_require() accepts the seed
* directly, so the seed is the real door and it could never be changed.
*
* That is fine for a staff position and wrong for a tenancy - and the rent
* system (admin_rent_claim, operator_addr, NST_RENT_NSU_PER_DAY) says
* tenancy is the intent. A tenant whose landlord holds a key to the safe is
* not a tenant.
*
* CONFIRMATION IS REQUIRED AND CASE-SENSITIVE, matching the WIPE prompt in
* Deploy.bat. Re-keying is irreversible with no recovery desk, so it must be
* un-runnable by accident rather than merely documented as dangerous.
*
* THE ADDRESS CHANGES, AND THAT HAS CONSEQUENCES THE CALLER MUST SEE.
* Addresses derive from seeds, so a new seed is a new wallet:
* - the crop's existing NSU stays at the OLD address, which the old seed
* still opens; move it deliberately, it is not swept
* - trade pays this crop's emission share to the address in its on-chain
* crop registry, which still names the OLD one until a successor is
* anchored there
* Both are reported in the response rather than left to be discovered.
*/
if ($api === 'admin_rekey' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
nsp_require();
if ((string)($_POST['confirm'] ?? '') !== 'REKEY') {
nsp_json([
'ok' => false,
'err' => 'Re-keying replaces this crop\'s panel seed permanently. There is no '
. 'recovery desk. POST confirm=REKEY to proceed.',
'confirm_required' => 'REKEY',
], 400);
}
$oldSeed = nsp_norm_seed((string)@file_get_contents(nsp_seed_file()));
$oldAddr = $oldSeed !== '' ? nsp_addr_from_seed($oldSeed) : '';
$new = nsp_generate_site_seed();
$newAddr = nsp_addr_from_seed($new);
if ($new === '' || $newAddr === '' || $newAddr === $oldAddr) {
nsp_json(['ok' => false, 'err' => 'seed generation failed'], 500);
}
/* Vault note first: it is the operator's offline copy, so if the second
* write fails the seed still exists somewhere other than this response.
* The old seed keeps working until site.seed itself is replaced, so a
* half-finished rotation locks nobody out. */
nsp_vault_site_seed_note($new);
if (@file_put_contents(nsp_seed_file(), $new . "\n", LOCK_EX) === false) {
nsp_json(['ok' => false, 'err' => 'could not write site.seed - crop unchanged, old seed still valid'], 500);
}
@chmod(nsp_seed_file(), 0600);
/* Read back before claiming success. Reporting a rotation that did not
* land would strand the lord with a seed the crop does not accept. */
$check = nsp_norm_seed((string)@file_get_contents(nsp_seed_file()));
if ($check !== nsp_norm_seed($new)) {
nsp_json(['ok' => false, 'err' => 'readback mismatch - rotation not confirmed'], 500);
}
nsp_json([
'ok' => true,
'seed' => $new,
'seed_shown_once' => true,
'old_addr' => $oldAddr,
'new_addr' => $newAddr,
'vault' => 'SITE_WALLET_SEED.txt',
'next_steps' => [
'SAVE THIS SEED OFFLINE NOW. It is shown once and there is no recovery desk.',
'The king\'s copy of the previous seed no longer opens this crop.',
'Your NSU is still at the OLD address - the old seed opens that wallet. '
. 'Transfer it to the new address deliberately; nothing is swept for you.',
'Trade still pays this crop\'s emission to the OLD address until a successor '
. 'is anchored in its on-chain crop registry.',
],
'msg' => 'Panel seed rotated. This crop is now yours alone.',
]);
}
if ($api === 'admin_get_source' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
nsp_require();
$raw = (string)file_get_contents(__FILE__);
nsp_json(['ok' => true, 'bytes' => strlen($raw), 'sha256' => hash('sha256', $raw), 'source' => $raw]);
}
if ($api === 'admin_put_source' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
nsp_require();
$src = (string)($_POST['source'] ?? '');
if (strlen($src) < 100 || strpos($src, ' false, 'err' => 'bad source'], 400);
$bak = __FILE__ . '.bak.' . time();
@copy(__FILE__, $bak);
if (file_put_contents(__FILE__, $src, LOCK_EX) === false) nsp_json(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500);
nsp_json(['ok' => true, 'msg' => 'replaced', 'backup' => basename($bak), 'sha256' => hash('sha256', $src)]);
}
nsp_json(['ok' => false, 'err' => 'unknown admin api'], 404);
return true;
}
function nsp_render_controlpanel(): void {
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store');
$site = 'Nosignup.Market';
echo '
';
echo '' . htmlspecialchars($site) . ' Control
';
echo '
DNA · MARKET · classified gold
';
echo '
' . htmlspecialchars($site) . ' · Control Panel
';
echo '
Site-local renter key for THIS crop. '
. 'Paste this crop\'s site wallet seed (vault SITE_WALLET_SEED.txt / data/site.seed). '
. 'UTTER control of THIS index.php (incl. replace). Not OS root. Independent vault. '
. 'This board has no visitor wallet or mint — listings forget ~30d; money/buy lives on nosignup.trade. '
. 'Panel door is site seed only. Leftover admin password files are unlinked. '
. 'Yearly wipe clears visitor data; site wallet seed is not rotated by wipe (rotate seed yourself if leaked).
';
echo '
';
echo '';
echo '';
echo '';
echo '';
echo '
Owner: seed auto-generated at first boot into data/site.seed + vault SITE_WALLET_SEED.txt (root pull). Paste seed → unlock. No password product path. No recovery desk. Market is not a mint. Not a visitor account. Four-corner classifieds only.
#i', $blk, $wm)) {
$where = html_entity_decode(trim(preg_replace('/\s+/', ' ', (string)$wm[1])), ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
$row = nsm_hunt_row($u, [
'title' => $title,
'price' => $price,
'where' => $where,
'body' => '',
'cat' => '',
'contact' => '',
]);
if ($row === null) continue;
$n = INF;
if (preg_match('/(\d+(?:[.,]\d+)?)/', $price, $nm)) $n = (float)str_replace(',', '', $nm[1]);
if ($n < $bestN) { $bestN = $n; $best = $row; }
}
return $best;
}
function nsm_hunt_row(string $url, array $got): ?array {
/* MARKET-HUNT-1: display-only. Require title + price + source_url. Never require or invent contact/where/cat. */
$url = nsm_harvest_url_ok($url) ?? '';
$title = nsm_san((string)($got['title'] ?? ''), NSM_MAX_TITLE);
$price = nsm_san((string)($got['price'] ?? ''), NSM_MAX_PRICE);
if ($url === '' || strlen($title) < 3 || $price === '') return null;
$cat = strtolower(preg_replace('/[^a-z]/', '', (string)($got['cat'] ?? '')) ?? '');
if (!in_array($cat, NSM_CATS, true)) $cat = '';
return [
'id' => hash('sha256', 'hunt|' . strtolower($url)),
'cat' => $cat,
'title' => $title,
'body' => nsm_san((string)($got['body'] ?? ''), NSM_MAX_BODY),
'price' => $price,
'where' => nsm_san((string)($got['where'] ?? ''), 80),
'contact' => nsm_san((string)($got['contact'] ?? ''), NSM_MAX_CONTACT),
'source_url' => $url,
'ts' => time(),
'hunt' => true,
];
}
/** Tiny purpose brain: listing_spam - bag-of-words, fail open (no external LLM). */
function nsm_brain_listing_spam(string $title, string $body): array {
$t = strtolower($title . ' ' . $body);
$hits = [];
$bad = [
'whatsapp' => 0.2, 'telegram.me' => 0.25, 'bit.ly' => 0.2, 'double your' => 0.35,
'guaranteed' => 0.2, 'wire transfer only' => 0.25, 'gift card' => 0.3,
'seed phrase' => 0.45, 'private key' => 0.45, 'act now' => 0.1, '100% free money' => 0.4,
'crypto giveaway' => 0.3, 'click here' => 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' => 'listing_spam', 'score' => round($score, 3), 'hits' => $hits, 'flag' => $score >= 0.55];
}
/** Tiny purpose brain: contact_risk - soft-scam tokens in freeform contact; fail open. */
function nsm_brain_contact_risk(string $contact): array {
$t = strtolower($contact);
$hits = [];
$bad = [
'seed phrase' => 0.5, 'private key' => 0.5, 'mnemonic' => 0.45, 'send btc first' => 0.4,
'wire only' => 0.25, 'gift card' => 0.35, 'western union' => 0.3, 'cashapp only' => 0.15,
'bit.ly' => 0.2, 't.me/' => 0.1, 'wa.me/' => 0.1, 'double your' => 0.35,
];
$score = 0.0;
foreach ($bad as $k => $w) {
if ($k !== '' && str_contains($t, $k)) {
$score += $w;
$hits[] = $k;
}
}
if (preg_match('/\b[5-9a-z]{50,}\b/i', $contact)) {
$hits[] = 'long_blob';
$score += 0.25;
}
if ($score > 1.0) {
$score = 1.0;
}
return ['brain' => 'contact_risk', 'score' => round($score, 3), 'hits' => $hits, 'flag' => $score >= 0.5];
}
function nsm_json(array $x, int $c = 200): void {
http_response_code($c);
header('Content-Type: application/json; charset=UTF-8');
header('Cache-Control: no-store');
echo json_encode($x, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
function nsm_data(): string {
$d = __DIR__ . DIRECTORY_SEPARATOR . 'data';
if (!is_dir($d)) @mkdir($d, 0755, true);
$ht = $d . DIRECTORY_SEPARATOR . '.htaccess';
if (!is_file($ht)) @file_put_contents($ht, "Require all denied\nDeny from all\n");
$idx = $d . DIRECTORY_SEPARATOR . 'index.html';
if (!is_file($idx)) @file_put_contents($idx, '');
return $d;
}
function nsm_file(): string { return __DIR__ . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'listings.jsonl'; }
/** Listing photos under data/media (parent data/ is web-denied). Flat {id}.{ext}. */
function nsm_media_dir(): string {
$d = nsm_data() . DIRECTORY_SEPARATOR . 'media';
if (!is_dir($d)) @mkdir($d, 0755, true);
return $d;
}
function nsm_media_path(string $id): ?string {
$id = preg_replace('/[^a-f0-9]/', '', strtolower($id)) ?? '';
if (strlen($id) < 16) return null;
$d = __DIR__ . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'media';
foreach (['jpg', 'jpeg', 'png', 'webp', 'gif'] as $e) {
$p = $d . DIRECTORY_SEPARATOR . $id . '.' . $e;
if (is_file($p)) return $p;
}
return null;
}
function nsm_media_unlink(string $id): void {
$id = preg_replace('/[^a-f0-9]/', '', strtolower($id)) ?? '';
if ($id === '') return;
foreach (glob(nsm_media_dir() . DIRECTORY_SEPARATOR . $id . '.*') ?: [] as $f) {
@unlink($f);
}
}
/**
* Ephemeral storage for L3 rate buckets (+ future disposable caches).
* Prefer /dev/shm when available (Linux tmpfs); never hard-require it.
* Fallback: data/ephemeral// on durable disk (still not product state).
* REFUSE: listings.jsonl, site.seed, admin.pass.hash, vault — those stay durable.
*/
function nsm_ephemeral_dir(string $kind = 'rate'): string {
$kind = preg_replace('/[^a-z0-9_\-]/', '', strtolower($kind)) ?: 'rate';
if (DIRECTORY_SEPARATOR === '/' && is_dir('/dev/shm') && is_writable('/dev/shm')) {
$base = '/dev/shm/nosignup_market_ip';
if ((is_dir($base) || @mkdir($base, 0700, true)) && is_writable($base)) {
if ($kind === 'rate') {
return $base;
}
$sub = $base . DIRECTORY_SEPARATOR . $kind;
if ((is_dir($sub) || @mkdir($sub, 0700, true)) && is_writable($sub)) {
return $sub;
}
return $base;
}
}
$d = nsm_data() . DIRECTORY_SEPARATOR . 'ephemeral' . DIRECTORY_SEPARATOR . $kind;
if (!is_dir($d)) {
@mkdir($d, 0700, true);
}
return $d;
}
/** Server-local privacy salt (data/ephemeral/.salt, dir 0700, file 0600). Not web-readable product state. */
function nsm_priv_salt(): string {
$d = nsm_data() . DIRECTORY_SEPARATOR . 'ephemeral';
if (!is_dir($d)) @mkdir($d, 0700, true);
$f = $d . DIRECTORY_SEPARATOR . '.salt';
if (is_file($f)) {
$s = trim((string)@file_get_contents($f));
if ($s !== '') return $s;
}
try {
$s = bin2hex(random_bytes(32));
} catch (Exception $e) {
$s = hash('sha256', uniqid('', true) . mt_rand());
}
if (@file_put_contents($f, $s, LOCK_EX) !== false) @chmod($f, 0600);
return $s;
}
/** Pseudonymous client tag - HMAC-SHA256(ip|extra, salt), 24 hex chars. Never store raw IP in filenames. */
function nsm_client_tag(string $ip, string $extra = ''): string {
return substr(hash_hmac('sha256', (string)$ip . '|' . $extra, nsm_priv_salt()), 0, 24);
}
function nsm_ip(): string {
// REMOTE_ADDR only - do not trust XFF; operator real_ip if reverse-proxied.
$ip = $_SERVER['REMOTE_ADDR'] ?? '0';
return substr(preg_replace('/[^0-9a-fA-F:.\-]/', '', $ip) ?? '0', 0, 64);
}
function nsm_rate_ok(string $ip): bool {
// Soft throttle (not mint) - LOCK_EX on write (match work/date; leave no room for error).
$f = nsm_ephemeral_dir('rate') . DIRECTORY_SEPARATOR . 'rate_' . nsm_client_tag($ip, 'rate') . '.json';
$now = time();
$hits = [];
if (is_file($f)) {
$j = json_decode((string)file_get_contents($f), true);
if (is_array($j)) $hits = array_values(array_filter($j, fn($t) => is_int($t) && ($now - $t) < NSM_RATE_WINDOW));
}
if (count($hits) >= NSM_RATE_MAX) return false;
$hits[] = $now;
@file_put_contents($f, json_encode($hits), LOCK_EX);
return true;
}
function nsm_read_all(): array {
$path = nsm_file();
if (!is_file($path)) return [];
$out = [];
$now = time();
$fh = fopen($path, 'rb');
if (!$fh) return [];
while (($line = fgets($fh)) !== false) {
$line = trim($line);
if ($line === '') continue;
$j = json_decode($line, true);
if (!is_array($j)) continue;
$ts = (int)($j['ts'] ?? 0);
if ($ts > 0 && ($now - $ts) > NSM_TTL_SECS) continue;
$out[] = $j;
}
fclose($fh);
return $out;
}
/** HMAC of browser-held edit_key — store hash only; never list edit_hash publicly. */
function nsm_edit_hash(string $key): string {
return hash_hmac('sha256', $key, nsm_priv_salt());
}
/** Public list/post shape: drop ownership secrets. */
function nsm_public_listing(array $L): array {
unset($L['edit_hash']);
return $L;
}
/**
* Rewrite full listings.jsonl under exclusive flock (edit/delete path).
* Same append-safe discipline as lazy compact — no tmp+rename race.
*/
function nsm_write_all(array $rows): bool {
$path = nsm_file();
$fh = @fopen($path, 'c+b');
if (!$fh) {
return false;
}
if (!flock($fh, LOCK_EX)) {
fclose($fh);
return false;
}
$lines = [];
foreach ($rows as $j) {
if (!is_array($j)) {
continue;
}
$enc = json_encode($j, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($enc !== false) {
$lines[] = $enc;
}
}
$body = $lines === [] ? '' : (implode("\n", $lines) . "\n");
if (!ftruncate($fh, 0)) {
flock($fh, LOCK_UN);
fclose($fh);
return false;
}
rewind($fh);
if ($body !== '' && fwrite($fh, $body) === false) {
flock($fh, LOCK_UN);
fclose($fh);
return false;
}
fflush($fh);
flock($fh, LOCK_UN);
fclose($fh);
return true;
}
/**
* Lazy single-request compact of listings.jsonl (chat-style tick, no cron).
* Keeps only non-expired rows (TTL 30d). Listings stay on durable disk — never RAM.
* APPEND-SAFE: exclusive flock on listings.jsonl for entire read→truncate→write;
* serializes with POST FILE_APPEND|LOCK_EX so concurrent posts are never lost.
* No tmp file, no rename (tmp+rename raced unlocked appends between read and swap).
*/
function nsm_listings_lazy_compact(bool $force = false): void {
$path = nsm_file();
if (!is_file($path)) {
return;
}
$sz = @filesize($path);
$over = is_int($sz) && $sz >= NSM_LISTINGS_COMPACT_BYTES;
if (!$force && !$over && mt_rand(1, NSM_LISTINGS_COMPACT_CHANCE) !== 1) {
return;
}
$fh = @fopen($path, 'c+b');
if (!$fh) {
return;
}
if (!flock($fh, LOCK_EX)) {
fclose($fh);
return;
}
$now = time();
$lines = [];
rewind($fh);
while (($line = fgets($fh)) !== false) {
$line = trim($line);
if ($line === '') {
continue;
}
$j = json_decode($line, true);
if (!is_array($j)) {
continue;
}
$ts = (int)($j['ts'] ?? 0);
if ($ts > 0 && ($now - $ts) > NSM_TTL_SECS) {
if (!empty($j['id'])) nsm_media_unlink((string)$j['id']);
continue;
}
$enc = json_encode($j, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($enc !== false) {
$lines[] = $enc;
}
}
$body = $lines === [] ? '' : (implode("\n", $lines) . "\n");
if (!ftruncate($fh, 0)) {
flock($fh, LOCK_UN);
fclose($fh);
return;
}
rewind($fh);
if ($body !== '' && fwrite($fh, $body) === false) {
flock($fh, LOCK_UN);
fclose($fh);
return;
}
fflush($fh);
flock($fh, LOCK_UN);
fclose($fh);
}
function nsm_san(string $s, int $max): string {
$s = trim(str_replace(["\r", "\0"], '', $s));
if (function_exists('mb_substr')) return mb_substr($s, 0, $max, 'UTF-8');
return substr($s, 0, $max);
}
if (isset($_GET['src']) || isset($_GET['download']) || (isset($_GET['api']) && $_GET['api'] === 'src')) {
$raw = (string)file_get_contents(__FILE__);
header('Content-Type: text/plain; charset=UTF-8');
header('X-Content-Type-Options: nosniff');
header('X-NS-Sha256: ' . hash('sha256', $raw));
if (isset($_GET['download'])) {
header('Content-Disposition: attachment; filename="nosignup-market.php"');
}
echo $raw;
exit;
}
$api = (string)($_GET['api'] ?? $_POST['api'] ?? '');
/* EMPIRE SETUP GATE → trade console until filled */
if ($api === '' && !isset($_GET['controlpanel']) && !isset($_GET['empire_setup']) && !isset($_GET['setup'])) {
$uri = (string)($_SERVER['REQUEST_URI'] ?? '');
if (!preg_match('#/(controlpanel|setup)/?(\?|$)#', $uri)) {
$dataDir = __DIR__ . DIRECTORY_SEPARATOR . 'data';
$ok = false;
$local = $dataDir . DIRECTORY_SEPARATOR . 'empire_setup.json';
if (is_file($local)) {
$lj = json_decode((string)@file_get_contents($local), true);
$ok = is_array($lj) && !empty($lj['filled']);
}
$cache = $dataDir . DIRECTORY_SEPARATOR . 'empire_setup_status_cache.json';
if (!$ok && is_file($cache)) {
$c = json_decode((string)@file_get_contents($cache), true);
if (is_array($c) && isset($c['ts'], $c['filled']) && (time() - (int)$c['ts']) < 60 && !empty($c['filled'])) {
$ok = true;
}
}
if (!$ok) {
$ctx = stream_context_create(['http' => ['timeout' => 2.5, 'ignore_errors' => true]]);
$httpsOn = (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') || ((int)($_SERVER['SERVER_PORT'] ?? 0) === 443) || (strtolower((string)($_SERVER['REQUEST_SCHEME'] ?? '')) === 'https') || (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https');
$scheme = $httpsOn ? 'https' : 'http';
$rawS = @file_get_contents($scheme . '://nosignup.trade/?api=empire_setup_status', false, $ctx);
$httpsHit = false;
if (is_string($rawS) && $rawS !== '') {
$j = json_decode($rawS, true);
if (is_array($j) && array_key_exists('filled', $j)) {
$httpsHit = true;
$ok = !empty($j['filled']);
if (!is_dir($dataDir)) {
@mkdir($dataDir, 0755, true);
}
@file_put_contents($cache, json_encode(['ts' => time(), 'filled' => $ok]) . "\n", LOCK_EX);
}
}
if (!$httpsHit && !$ok && is_file($cache)) {
$c = json_decode((string)@file_get_contents($cache), true);
if (is_array($c) && !empty($c['filled'])) {
$ok = true;
}
}
}
if (!$ok) {
header('Cache-Control: no-store');
header('Location: //nosignup.trade/?empire_setup=1', true, 302);
exit;
}
}
}
// Site seed genesis BEFORE any admin API (no land-grab window).
if ($api !== '' && str_starts_with($api, 'admin_')) {
nsp_ensure_site_seed();
nsp_handle_admin_api($api);
}
if ($api !== '' && $api !== 'src') {
if (in_array($api, ['post', 'edit', 'delete'], true)
&& ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
nsm_data();
}
/* WIRING-PARITY: public selfhash (trade-shape; no auth) */
if ($api === 'selfhash') {
nsm_json(['ok' => true, 'version' => NSM_VERSION, 'sha256' => @hash_file('sha256', __FILE__) ?: null, 'file' => basename(__FILE__)]);
}
if ($api === 'state') {
$all = nsm_read_all();
$counts = array_fill_keys(NSM_CATS, 0);
foreach ($all as $L) {
$c = (string)($L['cat'] ?? '');
if (isset($counts[$c])) $counts[$c]++;
}
nsm_json(['ok' => true, 'version' => NSM_VERSION, 'ttl_days' => intdiv(NSM_TTL_SECS, 86400), 'counts' => $counts, 'total' => count($all)]);
}
if ($api === 'list') {
$cat = strtolower(preg_replace('/[^a-z]/', '', (string)($_GET['cat'] ?? $_POST['cat'] ?? '')) ?? '');
/* RR350 P1-MARKET-LIST-CAT-PLAIN: unknown cat must not silently empty-filter as "ok" */
if ($cat !== '' && !in_array($cat, NSM_CATS, true)) {
nsm_json(['ok' => false, 'err' => 'Pick a category: free, items, realestate, or vehicles (or omit cat for all)', 'cat' => $cat], 400);
}
$q = strtolower(trim((string)($_GET['q'] ?? $_POST['q'] ?? '')));
$all = nsm_read_all();
$out = [];
$tokens = $q === '' ? [] : preg_split('/\s+/', $q, -1, PREG_SPLIT_NO_EMPTY);
if (!is_array($tokens)) $tokens = [];
foreach ($all as $L) {
if ($cat !== '' && ($L['cat'] ?? '') !== $cat) continue;
if ($q !== '') {
$title = strtolower((string)($L['title'] ?? ''));
$hay = $title . ' ' . strtolower((string)($L['body'] ?? '')) . ' ' . strtolower((string)($L['price'] ?? '')) . ' ' . strtolower((string)($L['where'] ?? ''));
$score = 0;
$matched = false;
if ($tokens) {
foreach ($tokens as $tok) {
$tok = (string)$tok;
if ($tok === '') continue;
if (strpos($hay, $tok) !== false) {
$matched = true;
$score += 1;
if (strpos($title, $tok) !== false) $score += 2; // title hits rank higher
}
}
}
// phrase fallback (single blob query)
if (!$matched && strpos($hay, $q) !== false) {
$matched = true;
$score = max($score, 1);
if (strpos($title, $q) !== false) $score += 2;
}
if (!$matched) continue;
$L['_score'] = $score;
}
$pid = (string)($L['id'] ?? '');
if ($pid !== '' && nsm_media_path($pid)) {
$L['photo'] = '?api=media&id=' . rawurlencode($pid);
}
$out[] = nsm_public_listing($L);
}
if ($q !== '') {
usort($out, static function ($a, $b) {
$sa = (int)($a['_score'] ?? 0);
$sb = (int)($b['_score'] ?? 0);
if ($sa !== $sb) return $sb <=> $sa;
return ((int)($b['ts'] ?? 0)) <=> ((int)($a['ts'] ?? 0));
});
foreach ($out as &$row) {
unset($row['_score']);
}
unset($row);
} else {
usort($out, fn($a, $b) => ((int)($b['ts'] ?? 0)) <=> ((int)($a['ts'] ?? 0)));
}
nsm_json(['ok' => true, 'cat' => $cat, 'n' => count($out), 'listings' => array_slice($out, 0, 200)]);
}
/** P1-MKT-POST-METHOD RR208: GET/HEAD must not fall through as unknown api (404).
* Sister crops work/date answer write routes with 405 POST only. */
if (($api === 'post' || $api === 'harvest' || $api === 'hunt') && strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') {
nsm_json(['ok' => false, 'err' => 'POST only'], 405);
}
if ($api === 'post' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
$ip = nsm_ip();
/* RR360 P1-MARKET-WRITE-ERR-PLAIN: visitor plain English (not "rate limit") */
if (!nsm_rate_ok($ip)) nsm_json(['ok' => false, 'err' => 'Too many posts from your network — wait a few minutes and try again', 'error' => 'rate_limited'], 429);
$cat = strtolower(preg_replace('/[^a-z]/', '', (string)($_POST['cat'] ?? '')) ?? '');
/* RR319 P1-MARKET-CAT-ERR-PLAIN */
if (!in_array($cat, NSM_CATS, true)) nsm_json(['ok' => false, 'err' => 'Pick a category: free, items, realestate, or vehicles'], 400);
$title = nsm_san((string)($_POST['title'] ?? ''), NSM_MAX_TITLE);
$body = nsm_san((string)($_POST['body'] ?? ''), NSM_MAX_BODY);
$price = nsm_san((string)($_POST['price'] ?? ''), NSM_MAX_PRICE);
$where = nsm_san((string)($_POST['where'] ?? ''), 80);
$contact = nsm_san((string)($_POST['contact'] ?? ''), NSM_MAX_CONTACT);
/* RR330 P1-MARKET-TITLE-BODY-PLAIN */
if (strlen($title) < 3) nsm_json(['ok' => false, 'err' => 'Title needs at least 3 characters'], 400);
if (strlen($body) < 3) nsm_json(['ok' => false, 'err' => 'Description needs at least 3 characters'], 400);
if ($price === '') $price = ($cat === 'free') ? 'FREE' : 'ASK';
if ($contact === '') nsm_json(['ok' => false, 'err' => 'contact required — put how people reach you on the listing (no account)'], 400);
$spam = nsm_brain_listing_spam($title, $body);
$cRisk = nsm_brain_contact_risk($contact);
// hard block only blatant scam copy; otherwise fail open with soft flag
if (($spam['score'] ?? 0) >= 0.9) {
nsm_json(['ok' => false, 'err' => 'listing rejected by listing_spam brain', 'brains' => ['listing_spam' => $spam, 'contact_risk' => $cRisk]], 400);
}
if (($cRisk['score'] ?? 0) >= 0.9) {
nsm_json(['ok' => false, 'err' => 'contact rejected by contact_risk brain', 'brains' => ['listing_spam' => $spam, 'contact_risk' => $cRisk]], 400);
}
$ts = time();
$id = hash('sha256', nsm_client_tag($ip, 'id') . '|' . $ts . '|' . $title . '|' . bin2hex(random_bytes(4)));
try {
$editKey = bin2hex(random_bytes(16));
} catch (Exception $e) {
$editKey = hash('sha256', uniqid('', true) . mt_rand());
}
$row = [
'id' => $id,
'cat' => $cat,
'title' => $title,
'body' => $body,
'price' => $price,
'where' => $where,
'contact' => $contact,
'ts' => $ts,
'exp' => $ts + NSM_TTL_SECS,
'ver' => 1,
'edit_hash' => nsm_edit_hash($editKey),
];
if (!empty($spam['flag'])) $row['spam'] = $spam;
if (!empty($cRisk['flag'])) $row['contact_risk'] = $cRisk;
// Optional photo: multipart field "photo". Soft-skip bad mime; hard reject only oversize.
if (!empty($_FILES['photo']) && is_array($_FILES['photo'])) {
$ferr = (int)($_FILES['photo']['error'] ?? UPLOAD_ERR_NO_FILE);
if ($ferr === UPLOAD_ERR_OK) {
$sz = (int)($_FILES['photo']['size'] ?? 0);
$tmp = (string)($_FILES['photo']['tmp_name'] ?? '');
if ($sz > NSM_MAX_PHOTO_BYTES) {
nsm_json(['ok' => false, 'err' => 'photo too large (max ~400KB)'], 413);
}
if ($sz > 0 && $tmp !== '' && is_uploaded_file($tmp)) {
$info = @getimagesize($tmp);
$mime = is_array($info) ? (string)($info['mime'] ?? '') : '';
$map = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'];
if (isset($map[$mime])) {
$ext = $map[$mime];
$dst = nsm_media_dir() . DIRECTORY_SEPARATOR . $id . '.' . $ext;
if (@move_uploaded_file($tmp, $dst)) {
$row['photo'] = 1;
}
}
}
}
}
$line = json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($line === false || file_put_contents(nsm_file(), $line . "\n", FILE_APPEND | LOCK_EX) === false) {
/* RR360: bare "disk" → plain English */
nsm_json(['ok' => false, 'err' => 'Could not save the listing — try again in a moment', 'error' => 'storage'], 500);
}
nsm_listings_lazy_compact(false);
$pub = nsm_public_listing($row);
if (!empty($row['photo'])) {
$pub['photo'] = '?api=media&id=' . rawurlencode($id);
}
// edit_key returned once to poster — browser stores; server keeps edit_hash only
nsm_json(['ok' => true, 'listing' => $pub, 'edit_key' => $editKey, 'msg' => 'posted - lives ~30 days, no account · this browser can edit/delete', 'brains' => ['listing_spam' => $spam, 'contact_risk' => $cRisk]]);
}
/** MARKET-HARVEST-1: one visitor POST, one public URL. GET/HEAD already 405 above. No queue. */
if ($api === 'harvest' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
$ip = nsm_ip();
if (!nsm_rate_ok($ip)) nsm_json(['ok' => false, 'err' => 'Too many posts from your network — wait a few minutes and try again', 'error' => 'rate_limited'], 429);
$rawUrl = (string)($_POST['url'] ?? '');
$url = nsm_harvest_url_ok($rawUrl);
if ($url === null) nsm_json(['ok' => false, 'err' => 'Need one public http(s) listing URL (no private/local host)'], 400);
foreach (nsm_read_all() as $L) {
if (isset($L['source_url']) && strtolower((string)$L['source_url']) === strtolower($url)) {
nsm_json(['ok' => false, 'err' => 'That URL is already on the board'], 409);
}
}
$html = nsm_harvest_fetch($url);
if ($html === null) nsm_json(['ok' => false, 'err' => 'Could not fetch that URL'], 400);
$got = nsm_harvest_extract($html);
$cat = strtolower(preg_replace('/[^a-z]/', '', (string)($got['cat'] ?? '')) ?? '');
$title = nsm_san((string)($got['title'] ?? ''), NSM_MAX_TITLE);
$body = nsm_san((string)($got['body'] ?? ''), NSM_MAX_BODY);
$price = nsm_san((string)($got['price'] ?? ''), NSM_MAX_PRICE);
$where = nsm_san((string)($got['where'] ?? ''), 80);
$contact = nsm_san((string)($got['contact'] ?? ''), NSM_MAX_CONTACT);
$miss = [];
if (!in_array($cat, NSM_CATS, true)) $miss[] = 'cat';
if (strlen($title) < 3) $miss[] = 'title';
if (strlen($body) < 3) $miss[] = 'body';
if ($price === '') $miss[] = 'price';
if ($where === '') $miss[] = 'where';
if ($contact === '') $miss[] = 'contact';
if ($miss !== []) nsm_json(['ok' => false, 'err' => 'URL is missing required fields: ' . implode(', ', $miss) . ' — no stub, no invented contact'], 400);
$spam = nsm_brain_listing_spam($title, $body);
$cRisk = nsm_brain_contact_risk($contact);
if (($spam['score'] ?? 0) >= 0.9) {
nsm_json(['ok' => false, 'err' => 'listing rejected by listing_spam brain', 'brains' => ['listing_spam' => $spam, 'contact_risk' => $cRisk]], 400);
}
if (($cRisk['score'] ?? 0) >= 0.9) {
nsm_json(['ok' => false, 'err' => 'contact rejected by contact_risk brain', 'brains' => ['listing_spam' => $spam, 'contact_risk' => $cRisk]], 400);
}
$ts = time();
$id = hash('sha256', nsm_client_tag($ip, 'id') . '|harvest|' . $ts . '|' . $url . '|' . bin2hex(random_bytes(4)));
try {
$editKey = bin2hex(random_bytes(16));
} catch (Exception $e) {
$editKey = hash('sha256', uniqid('', true) . mt_rand());
}
$row = [
'id' => $id,
'cat' => $cat,
'title' => $title,
'body' => $body,
'price' => $price,
'where' => $where,
'contact' => $contact,
'source_url' => $url,
'ts' => $ts,
'exp' => $ts + NSM_TTL_SECS,
'ver' => 1,
'edit_hash' => nsm_edit_hash($editKey),
];
if (!empty($spam['flag'])) $row['spam'] = $spam;
if (!empty($cRisk['flag'])) $row['contact_risk'] = $cRisk;
$line = json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($line === false || file_put_contents(nsm_file(), $line . "\n", FILE_APPEND | LOCK_EX) === false) {
nsm_json(['ok' => false, 'err' => 'Could not save the listing — try again in a moment', 'error' => 'storage'], 500);
}
nsm_listings_lazy_compact(false);
$pub = nsm_public_listing($row);
nsm_json(['ok' => true, 'listing' => $pub, 'edit_key' => $editKey, 'msg' => 'harvested - lives ~30 days, no account · this browser can edit/delete', 'brains' => ['listing_spam' => $spam, 'contact_risk' => $cRisk]]);
}
/** MARKET-HUNT-2: this request only. One or two public fetches. Display-only. No jsonl. */
if ($api === 'hunt' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
$ip = nsm_ip();
if (!nsm_rate_ok($ip)) nsm_json(['ok' => false, 'err' => 'Too many posts from your network — wait a few minutes and try again', 'error' => 'rate_limited'], 429);
$q = trim((string)($_POST['q'] ?? ''));
if (strlen($q) < 3 || strlen($q) > 80) nsm_json(['ok' => false, 'err' => 'Hunt needs 3–80 characters'], 400);
$search = nsm_harvest_url_ok('https://toronto.craigslist.org/search/sss?query=' . rawurlencode($q) . '&sort=priceasc');
$best = null;
if ($search !== null) {
$index = nsm_harvest_fetch($search);
if (is_string($index) && $index !== '') {
$best = nsm_hunt_cards($index);
if ($best === null) {
$u = nsm_hunt_ddg_pick($index);
if ($u !== null) {
$html = nsm_harvest_fetch($u);
if ($html !== null) $best = nsm_hunt_row($u, nsm_harvest_extract($html));
}
}
}
}
$listings = $best ? [$best] : [];
nsm_json(['ok' => true, 'listings' => $listings, 'n' => count($listings), 'hunt' => true]);
}
/** P1-MKT-EDIT-DELETE: browser-held edit_key (no account). GET/HEAD → 405. */
if (($api === 'edit' || $api === 'delete') && strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') {
nsm_json(['ok' => false, 'err' => 'POST only'], 405);
}
if ($api === 'edit' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
$ip = nsm_ip();
/* RR360 P1-MARKET-WRITE-ERR-PLAIN: visitor plain English (not "rate limit") */
if (!nsm_rate_ok($ip)) nsm_json(['ok' => false, 'err' => 'Too many posts from your network — wait a few minutes and try again', 'error' => 'rate_limited'], 429);
$id = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['id'] ?? ''))) ?? '';
$editKey = trim((string)($_POST['edit_key'] ?? ''));
/* RR339 P1-MARKET-EDIT-KEY-PLAIN */ if (strlen($id) < 16 || $editKey === '') nsm_json(['ok' => false, 'err' => 'Need the listing id and the edit key this browser got when you posted (no account)'], 400);
$want = nsm_edit_hash($editKey);
$all = nsm_read_all();
$found = false;
$idx = -1;
foreach ($all as $i => $L) {
if ((string)($L['id'] ?? '') !== $id) continue;
$found = true;
$idx = (int)$i;
$eh = (string)($L['edit_hash'] ?? '');
if ($eh === '' || !hash_equals($eh, $want)) {
nsm_json(['ok' => false, 'err' => 'edit_key invalid for this listing (wrong browser or key lost)'], 403);
}
break;
}
/* RR357 P1-MARKET-LISTING-NF-PLAIN */ if (!$found) nsm_json(['ok' => false, 'err' => 'That listing is gone or expired — refresh the board'], 404);
$cat = strtolower(preg_replace('/[^a-z]/', '', (string)($_POST['cat'] ?? $all[$idx]['cat'] ?? '')) ?? '');
/* RR319 P1-MARKET-CAT-ERR-PLAIN */
if (!in_array($cat, NSM_CATS, true)) nsm_json(['ok' => false, 'err' => 'Pick a category: free, items, realestate, or vehicles'], 400);
$title = nsm_san((string)($_POST['title'] ?? ''), NSM_MAX_TITLE);
$body = nsm_san((string)($_POST['body'] ?? ''), NSM_MAX_BODY);
$price = nsm_san((string)($_POST['price'] ?? ''), NSM_MAX_PRICE);
$where = nsm_san((string)($_POST['where'] ?? ''), 80);
$contact = nsm_san((string)($_POST['contact'] ?? ''), NSM_MAX_CONTACT);
/* RR330 P1-MARKET-TITLE-BODY-PLAIN */
if (strlen($title) < 3) nsm_json(['ok' => false, 'err' => 'Title needs at least 3 characters'], 400);
if (strlen($body) < 3) nsm_json(['ok' => false, 'err' => 'Description needs at least 3 characters'], 400);
if ($price === '') $price = ($cat === 'free') ? 'FREE' : 'ASK';
if ($contact === '') nsm_json(['ok' => false, 'err' => 'contact required — put how people reach you on the listing (no account)'], 400);
$spam = nsm_brain_listing_spam($title, $body);
$cRisk = nsm_brain_contact_risk($contact);
if (($spam['score'] ?? 0) >= 0.9) {
nsm_json(['ok' => false, 'err' => 'listing rejected by listing_spam brain', 'brains' => ['listing_spam' => $spam, 'contact_risk' => $cRisk]], 400);
}
if (($cRisk['score'] ?? 0) >= 0.9) {
nsm_json(['ok' => false, 'err' => 'contact rejected by contact_risk brain', 'brains' => ['listing_spam' => $spam, 'contact_risk' => $cRisk]], 400);
}
$row = $all[$idx];
$row['cat'] = $cat;
$row['title'] = $title;
$row['body'] = $body;
$row['price'] = $price;
$row['where'] = $where;
$row['contact'] = $contact;
$row['ver'] = (int)($row['ver'] ?? 1) + 1;
$row['edited'] = time();
if (!empty($spam['flag'])) $row['spam'] = $spam; else unset($row['spam']);
if (!empty($cRisk['flag'])) $row['contact_risk'] = $cRisk; else unset($row['contact_risk']);
if (!empty($_FILES['photo']) && is_array($_FILES['photo'])) {
$ferr = (int)($_FILES['photo']['error'] ?? UPLOAD_ERR_NO_FILE);
if ($ferr === UPLOAD_ERR_OK) {
$sz = (int)($_FILES['photo']['size'] ?? 0);
$tmp = (string)($_FILES['photo']['tmp_name'] ?? '');
if ($sz > NSM_MAX_PHOTO_BYTES) {
nsm_json(['ok' => false, 'err' => 'photo too large (max ~400KB)'], 413);
}
if ($sz > 0 && $tmp !== '' && is_uploaded_file($tmp)) {
$info = @getimagesize($tmp);
$mime = is_array($info) ? (string)($info['mime'] ?? '') : '';
$map = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'];
if (isset($map[$mime])) {
nsm_media_unlink($id);
$ext = $map[$mime];
$dst = nsm_media_dir() . DIRECTORY_SEPARATOR . $id . '.' . $ext;
if (@move_uploaded_file($tmp, $dst)) {
$row['photo'] = 1;
}
}
}
}
}
$all[$idx] = $row;
if (!nsm_write_all($all)) nsm_json(['ok' => false, 'err' => 'Could not save the listing — try again in a moment', 'error' => 'storage'], 500);
$pub = nsm_public_listing($row);
if (nsm_media_path($id)) {
$pub['photo'] = '?api=media&id=' . rawurlencode($id);
}
nsm_json(['ok' => true, 'listing' => $pub, 'msg' => 'updated', 'brains' => ['listing_spam' => $spam, 'contact_risk' => $cRisk]]);
}
if ($api === 'delete' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
$ip = nsm_ip();
/* RR360 P1-MARKET-WRITE-ERR-PLAIN: visitor plain English (not "rate limit") */
if (!nsm_rate_ok($ip)) nsm_json(['ok' => false, 'err' => 'Too many posts from your network — wait a few minutes and try again', 'error' => 'rate_limited'], 429);
$id = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['id'] ?? ''))) ?? '';
$editKey = trim((string)($_POST['edit_key'] ?? ''));
/* RR339 P1-MARKET-EDIT-KEY-PLAIN */ if (strlen($id) < 16 || $editKey === '') nsm_json(['ok' => false, 'err' => 'Need the listing id and the edit key this browser got when you posted (no account)'], 400);
$want = nsm_edit_hash($editKey);
$all = nsm_read_all();
$keep = [];
$removed = false;
foreach ($all as $L) {
if ((string)($L['id'] ?? '') !== $id) {
$keep[] = $L;
continue;
}
$eh = (string)($L['edit_hash'] ?? '');
if ($eh === '' || !hash_equals($eh, $want)) {
nsm_json(['ok' => false, 'err' => 'edit_key invalid for this listing (wrong browser or key lost)'], 403);
}
$removed = true;
nsm_media_unlink($id);
}
/* RR357 P1-MARKET-LISTING-NF-PLAIN */ if (!$removed) nsm_json(['ok' => false, 'err' => 'That listing is gone or expired — refresh the board'], 404);
if (!nsm_write_all($keep)) nsm_json(['ok' => false, 'err' => 'Could not save the listing — try again in a moment', 'error' => 'storage'], 500);
nsm_json(['ok' => true, 'deleted' => $id, 'msg' => 'deleted']);
}
if ($api === 'media') {
/* RR347 P1-MARKET-MEDIA-MISS-PLAIN: missing/expired photo → JSON not empty HTML 404 */
$mid = (string)($_GET['id'] ?? '');
if ($mid === '') nsm_json(['ok' => false, 'err' => 'Need a listing photo id (?api=media&id=...)'], 400);
$mp = nsm_media_path($mid);
if (!$mp) nsm_json(['ok' => false, 'err' => 'Photo not found or expired'], 404);
$mt = (int)@filemtime($mp);
if ($mt > 0 && (time() - $mt) > NSM_TTL_SECS) { @unlink($mp); nsm_json(['ok' => false, 'err' => 'Photo not found or expired'], 404); }
$ext = strtolower(pathinfo($mp, PATHINFO_EXTENSION));
$mime = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'gif' => 'image/gif'][$ext] ?? 'application/octet-stream';
header('Content-Type: ' . $mime);
header('Cache-Control: public, max-age=3600');
header('X-Content-Type-Options: nosniff');
header('Content-Length: ' . (string)@filesize($mp));
readfile($mp);
exit;
}
/* RR335 P1-MARKET-UNKNOWN-API-PLAIN: bare "unknown api" → plain route hints (no face HTML) */
nsm_json([
'ok' => false,
'err' => 'Unknown market API — use selfhash, list, post, harvest, hunt, edit, delete, media, or open the classifieds face',
'api' => $api,
], 404);
}
$wantPanel = isset($_GET['controlpanel']) || (isset($_SERVER['REQUEST_URI']) && preg_match('#/controlpanel/?(\?|$)#', (string)$_SERVER['REQUEST_URI']));
if ($wantPanel && (string)($_GET['api'] ?? $_POST['api'] ?? '') === '') {
nsp_ensure_site_seed();
nsp_render_controlpanel();
}
?>
Nosignup.Market · keyword classifieds (not best-match)
Hunt to fill squares. Cheapest at center. Or tap a corner to post.
Post ad
No accounts. Currency is whatever you write in Price (FREE / $40 / BTC / trade). Listings expire ~30 days. Contact on the listing is how people reach you — public on the card, not a login.
Classifieds · No Signup
Classifieds
Four sectorsFree · Items · Real Estate · Vehicles — tap to post
SearchType — listings flood by keyword token rank (not best-match)
No accountNo signup wall · one PHP file · mirrors welcome
No wallet hereOptional donate · money/buy on nosignup.trade · no local balance
18+ · experimental · unmoderated · no recovery desktrust local · enter at your own risk.
Options
Site options stay in this browser only. Not an account.
Free tributaries. Host a copy · help the network · zero cut · no price power. Free mirrors help the network; hosting is unpaid until parent-proven hits (no free daily host pay); host so the swarm stays hard to kill. No free pay on market; Money/buy lives on nosignup.trade. One .php file — drop on any PHP host.