A community marketplace without a database or payment provider: the June-Markt
Today the June-Markt went live on crowdware.info/markt. It's a marketplace for the community around June (Ğ1), a free currency. Members offer honey, massages, websites or guitar lessons, post requests such as "band looki
Today the June-Markt went live on crowdware.info/markt. It's a marketplace for the community around June (Ğ1), a free currency. Members offer honey, massages, websites or guitar lessons, post requests such as "band looking for a guitarist", and pay each other directly from wallet to wallet.
It runs on ForgeCMS, my small CMS written in Go, as a new module. There's no database, no payment provider, and the platform never holds a single coin of anybody's money. The operator can still charge a fee on sales.
This morning I bought a 1 Ğ1 test item from my own listing. About 90 seconds after the transfer, the order was marked as paid, both emails had gone out, and 0.03 Ğ1 of market fee was booked on the vendor's fee account.
👉 For operators: https://crowdware.info/market-module
Why a market for June?
Ğ1 has been running since 2017. Every member creates the same small share of new money each day, the universal dividend, so it works a bit like a basic income built into the currency. Since March 2026 it runs on Duniter v2, a Substrate-based blockchain.
A currency is only as useful as the things you can buy with it. Ğchange, a classifieds site for June, already exists. I wanted something that fits into local websites, for example one market per association. I also wanted the site to see payments by itself, so it can do more than show ads.
What it does
For visitors:
- a list of listings with photo, price, place and hashtags, sorted into goods, services and requests ("wanted")
- search by text, postcode or place and kind, plus hashtags like
#honig,#massage,#bandsuchtgitarist, with a tag cloud and a few quick filters chosen by the operator - a contact form on every listing that mails the vendor and keeps the vendor's address hidden
- a "report" button on every listing, because the EU Digital Services Act requires a notice-and-action mechanism
- direct purchase on listings that allow it
For vendors: login by email link (no password), a profile with an optional Ğ1 wallet, up to four photos per listing, Markdown descriptions, and pause, renew and delete with one click. Listings go offline after 90 days, with a reminder a week before.
For the operator: moderation (the first listing of every new vendor waits for approval), reports, blocking, and a fee on direct sales.
Like everything in ForgeCMS, it's server-rendered HTML with Bootstrap classes and no JavaScript.
Hashtags instead of categories
My first plan had fixed categories in the config. Then I realised a community market is too varied for that. Someone selling honey, someone offering a massage and someone looking for a band don't fit into one category tree. So every listing gets one to five free hashtags:
#Software, massage #Mann_sucht_Frau
They are normalised to lower case, without #, and limited to 2–30 Unicode letters, digits or _. The list filters by any combination of them.
The ✓ badge: trust from the Web of Trust
Ğ1 members are certified by other members, so the currency has a Web of Trust built in. The public indexer tells you whether a wallet belongs to a certified member:
accounts(filter: {id: {equalTo: $wallet}}) {
nodes { identity { name isMember } }
}
Vendors whose wallet belongs to a member get a "✓ Ğ1 member: name" badge. It doesn't guarantee the offer, but on a community market it is a strong signal against fake accounts. A new wallet only shows up after the vendor confirms it by email, so a stolen session cookie can't silently redirect payments.
Direct purchase: the money never touches the server
This was the hard design question. I wanted the operator to earn a fee, say 3 %. The obvious way is that the platform collects the money and forwards 97 % to the vendor. That has two problems:
- The server would need a private key, so a hacked server would mean stolen money.
- In Germany, collecting and forwarding other people's money is regulated as a payment service. That's not something a small community site wants to deal with.
So the buyer pays the vendor's wallet directly, and the fee is billed afterwards:
- The buyer orders on the listing page and gets an order number, e.g.
MK-EZMDY3GY. - They pay the vendor's wallet with that number in the transfer comment.
- A watcher goroutine sees the payment, marks the order paid, books
amount × 3 %on the vendor's fee account, and mails both sides. - On the 1st of every month each vendor gets a statement
FEE-…and pays it to the market wallet, again with the number in the comment. Totals under 1 Ğ1 are carried over. - If a statement is still unpaid 30 days later, the vendor's listings are hidden until it is paid.
Vendors opt in per listing. The "allow direct purchase" checkbox also accepts the fee, so existing listings stay contact-only and fee-free.
Watching many wallets with one query
The shop module watches one wallet. The market has to watch every vendor wallet with an open order, plus the market wallet while a statement is open. The Duniter indexer is PostGraphile, so one in filter covers all of them:
transfers(orderBy: BLOCK_NUMBER_ASC,
filter: {toId: {in: $wallets},
blockNumber: {greaterThan: $from, lessThanOrEqualTo: $to}}) {
nodes { id blockNumber amount fromId toId comment { remark } }
}
Once a minute the watcher reads the blocks since the last run, minus 10 confirmations. An order ID only counts on a transfer to that order's wallet. A statement ID only counts on the market wallet. Partial payments add up, a transfer is never booked twice, and the last processed block lives in a small state file. Vendor wallets get plenty of unrelated transfers, and those are simply ignored.
No database, still
Everything is SML, my own small config format, written atomically next to the binary:
Listing {
id: "k3f9qxab"
kind: "goods"
title: "Blütenhonig 500 g"
tags: "honig lebensmittel"
price: 800 // centimes: 8.00 Ğ1
unit: "Glas"
status: "active"
buy: true
Image { file: "k3f9qxab-abcdefgh.jpg" }
}
On start the store loads everything into memory, and every change is a temp file plus rename under one mutex. For a community market with hundreds of listings that's plenty, and backups are just rsync.
I did hit one classic bug. The list functions called a filter callback while holding the store mutex. When the filter started asking "is this vendor's fee overdue?", which needs the same mutex, the test froze. Now the store copies under the lock and filters outside it.
User content is untrusted content
A few things that matter more on a marketplace than on a normal site:
- Photos are decoded one at a time (to protect a small VPS from memory spikes), turned upright using the EXIF orientation, scaled to 1600 px and re-encoded. That drops all metadata, including the GPS position of your kitchen.
-
Markdown in listings goes through its own goldmark renderer, not the site's. It drops raw HTML, empties
javascript:links, addsrel="nofollow ugc noopener", turns images into plain links so nothing third-party gets loaded, and starts headings at<h3>. - The contact form doesn't send a copy to the sender. Otherwise anyone could use it to mail arbitrary text to arbitrary addresses.
- Removing a listing or blocking a vendor mails the vendor the reason, as Art. 17 DSA requires. A reply to that mail goes to the moderator.
Try it
- The market: https://crowdware.info/markt
- For operators: https://crowdware.info/market-module
- What is June? https://crowdware.info/june
- ForgeCMS source (GPLv3): https://codeberg.org/CrowdWare/ForgeCMS
The market module is a one-time license for €49 or 49 June (Ğ1). If your association, village or community wants its own market, get in touch.
If you already trade in June, post something on the June-Markt. I'd love to see what turns up under #.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.