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 incontent/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),
noindexon account, private, form and error pages,/robots.txtand a/sitemap.xmlwith 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 fromcontent/site-key.asconce 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 theMessagesmockup: 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, theZB-<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
Ordermockup (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/orderswith 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/, andGET /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/listingswith 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_ENABLEDsetting. -
Money helpers: ZEC as integer zatoshis, fiat as decimal strings (no floating point).
-
Zcash receiving-address validation with the official
zcash_addresscrate 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_NETWORKandALLOWED_ADDRESS_TYPESsettings. 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/usersservice: public profiles without private fields, self-onlypatch, 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;
429withRetry-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_SECRETconfiguration, 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/healthchecks 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) anddocker-compose.dev.yml. - Vitest unit + integration tests (testcontainers), ESLint + Prettier, GitHub Actions CI.
- Feathers v5 + Koa + strict TypeScript (ESM) skeleton; services exposed as REST under
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-Forentry is trusted (maxIpsCount = 1).