Skip to content

Changelog

All notable changes to this project are documented here. Format based on Keep a Changelog.

[Unreleased]

Changed

  • The Zcash logo is the site logo and favicon.
  • Softer light theme: light grey page, white cards and panels with a faint shadow.
  • New interface following the "neutral marketplace" design (design/): white surfaces and ZEC gold for actions, system fonts, rounded cards, a navy dark theme. Header with the search bar, the ZEC price, a theme switch (a plain form, no JavaScript) and an account menu; category bar; footer with link columns. Listing cards with a "New" tag, a "Direct" payment badge, the price and its counter-value, the place and the seller's rating. Listing page with gallery, details and a buy box, the seller card and more listings from the seller. Numbered form cards and type tiles for posting. Layout for wide screens (4 columns), tablets and phones. Every existing page rebuilt from reusable components.
  • Posting a listing is now a single form: the type is picked at the top, and the fields of the other types are hidden with CSS only (everything still works without it). Exchange listings always use a range.

Added

  • Blog at /blog/: posts written in Markdown in content/blog/ with a short front matter (title, description, dates, tags, thumbnail), signed "ZecMarket Team", with a table of contents, related articles and an RSS feed (/blog/feed.xml). Posts dated in the future stay hidden in production. First five guides: buying and selling with Zcash, shielded addresses, non-custodial marketplaces, ZIP-321 payment links and safe peer-to-peer trading.

  • Search engines: canonical URLs, Open Graph tags, Schema.org JSON-LD (organization, website, blog posts, breadcrumbs), noindex on account, private, form and error pages, /robots.txt and a /sitemap.xml with the public pages, posts, non-empty categories and active listings.

  • Site pages written in Markdown in the repo (content/pages/<page>.md): FAQ, about and terms, with anchors and a table of contents; /changelog/ shows this file. /pgp/ publishes the team's PGP key from content/site-key.asc once one is added. Every footer link now leads somewhere.

  • Reports: "Report" forms (no JavaScript) on listings, profiles, orders and reviews, with a reason from a list and optional details; one open report per person and target; api/reports, read by moderators only.

  • Moderation area /admin/ for moderators and admins: open reports, listings held by a banned keyword (publish or remove), open disputes (closed with a written outcome shown to both parties; the order stays disputed since nothing can be refunded), member pages (warning badge, ban with reason: login refused, sessions ended, active listings paused; unban), review removal (average recomputed), and a log of every action with its reason. Staff members cannot be moderated from the interface.

  • Private messages, end-to-end encrypted with PGP (manual mode): people encrypt with their own tool and paste the armored block; the server only accepts a PGP message encrypted for the recipient's key and stores nothing else. Conversations on /messages/ and /messages/<username>/ (after the Messages mockup: inbox, thread, recipient's key fingerprint and download link, GnuPG help), optional link to an order between the two people, unread count next to an envelope in the header. Reached from the listing buy box, the order page and profiles. Moderators cannot read conversations. api/messages.

  • Reviews: once an order is delivered, buyer and seller each rate the other from 1 to 5 stars with an optional comment, once, within 60 days. Authors are shown masked (b***r) and the API never tells who wrote a review nor for which order. The average and count of each member are recomputed from their reviews. Order page: star picker that works without JavaScript, "Reviewed" step; listing page: the seller's latest reviews from buyers; profile: every review received. api/reviews, read-only for everyone once written.

  • Payment request for accepted orders (ZIP-321): zcash: link with the locked amount, the ZB-<order> memo (base64url) and a message, generated at acceptance and hidden again if the order ends unpaid. Shown to the buyer as an "Open in wallet" button and a QR code drawn server-side as inline SVG (black on white in both themes, no JavaScript needed). "Copy address" button when JavaScript is on.

  • Orders with direct payment (products, services, housing and digital goods, at a fixed price or on offer): order form in the listing buy box (delivery, quantity, proposed amount, optional note that must be a PGP message encrypted for the seller), order page following the Order mockup (steps, amount and memo to pay, seller's address shown from acceptance, irreversibility warning, actions for each party, activity), purchases and sales on /orders/, unread orders counted next to "Orders" in the header.

  • Order lifecycle in one tested transition table (pending → accepted → paid → completed; rejected, cancelled, expired, disputed): each step checks who acts, is applied atomically and logged. The amount in ZEC is locked at acceptance (fiat prices converted at the current rate, refused when the rate is over an hour old). "Paid" is declared by the buyer, never verified, and both parties are told. Accepted orders left unpaid expire after 72 hours (ORDERS_EXPIRY_ENABLED, one instance at a time). api/orders with the same rules.

  • Stock on product listings: reserved when an order is accepted, given back if it is cancelled or expires.

  • Search (/search/, /search/:type/, /category/:category/, and GET /api/search): full-text query, type (several at once for the swap desk), category, tag, seller, country or online, delivery, digital goods; sorted by relevance, date, price (fiat prices converted at the current rate, exactly, in the database) or seller rating, or at random; 5, 50 or 100 per page. Only active listings, only public fields.

  • Home page: new listings (tagged "New" for 48 hours) and the latest jobs and wanted ads; listing counts per category in the search filters.

  • npm run seed: demo accounts and listings for local development (refused in production).

  • Sign-up: terms acceptance and an optional recovery contact.

  • Stylesheet served as one file named by its content hash, cached for good.

  • Listing photos: up to 8 per listing, re-encoded to JPEG with every metadata block removed (EXIF, GPS), real format checked by decoding (SVG and decompression bombs refused), 5 MB max, thumbnails, stored by content hash and served with long-lived caching. Cover choice and removal. Multipart bodies accepted only on the upload route, which checks the uploader before reading the file.

  • Listings (all types except auctions): type picker, forms adapted to each type, detail page with safe Markdown and the approximate counter-value at the current rate, edit, pause, mark as sold, remove. Drafts, legal confirmation to publish, banned keywords sending a listing to moderation, PGP key required, receiving address only for authors paid in ZEC and never shown to other people. api/listings with the same rules. Sellers' listings on their profile.

  • ZEC/fiat rates (USD, EUR, GBP, CHF) every 5 minutes from Coinbase, Kraken and CoinGecko: median of the sources that answer, last known value kept when all fail, flagged stale after 1 hour; one instance refreshes at a time. Public read-only GET /api/rates. RATES_ENABLED setting.

  • Money helpers: ZEC as integer zatoshis, fiat as decimal strings (no floating point).

  • Zcash receiving-address validation with the official zcash_address crate compiled to WebAssembly (reproducible build, checked in CI): mainnet unified addresses with an Orchard or Sapling receiver only; transparent, legacy Sapling, TEX and wrong-network addresses refused. ZCASH_NETWORK and ALLOWED_ADDRESS_TYPES settings. Tested against the 60 official test vectors.

  • Public profiles (/user/:username/): member since, activity by week/month only, rating, badges, PGP fingerprint and key, downloadable as .asc.

  • Settings: PGP public key (checked server-side: private keys refused and never echoed back, revoked, expired, sign-only or several keys refused), recovery contact visible to its owner and moderators only, notifications, theme, password change that logs out other devices.

  • Password reset without email: a one-time code encrypted to the account's PGP key (30 minutes, single use).

  • Accounts: signup (username + password only), login and logout pages. Argon2id hashing, case-insensitive unique usernames, reserved names, generic login errors with constant-time checks, rate limiting per IP and per targeted account, banned accounts refused and logged out.

  • api/users service: public profiles without private fields, self-only patch, no signup through the API.

  • Sessions stored in Redis under a SHA-256 of the cookie, rotated on login, "log out everywhere" support.

  • CSRF protection (signed double-submit token) on every unsafe request, forms and /api/.

  • Redis rate limiting by daily-salted IP hash and by account; 429 with Retry-After.

  • The site is in English only; interface text lives in one dictionary (src/i18n/locales/en.json), so a language can be added later.

  • One-shot flash messages after form redirects.

  • Restricted Markdown rendering for user content (no raw HTML, no images, safe links).

  • SESSION_SECRET configuration, required in production; readable configuration errors at startup.

  • Phase 0 foundation:

    • Feathers v5 + Koa + strict TypeScript (ESM) skeleton; services exposed as REST under /api/.
    • Environment-based configuration validated with TypeBox (config/, .env.example).
    • MongoDB and Redis connections with graceful shutdown; GET /api/health checks both.
    • Server-side rendering with Nunjucks: layout, home page, HTML error pages (JSON under /api/).
    • Security headers: strict CSP (default-src 'self', no inline scripts), Referrer-Policy: no-referrer, HSTS only when served over HTTPS.
    • Hand-written stylesheet with light/dark/auto theme stored in a cookie (works without JavaScript).
    • Trailing-slash redirects for SSR routes.
    • Pure modules: listing types, categories, 4-character base62 short IDs with collision retry.
    • MongoDB index definitions and an idempotent migration script (npm run migrate).
    • Docker image (multi-stage), docker-compose.yml (app, mongo, redis) and docker-compose.dev.yml.
    • Vitest unit + integration tests (testcontainers), ESLint + Prettier, GitHub Actions CI.

Fixed

  • Short ID collision retry now also recognises duplicate-key errors wrapped by the Feathers MongoDB adapter.

Security

  • Only the reverse proxy's X-Forwarded-For entry is trusted (maxIpsCount = 1).