Building MyZubster’s Internal MYZ Payments, Marketplace Knowledge Layer, and a Safer Ledger
Over the last few days, we made a fairly deep set of changes to MyZubster.
This was not simply about adding a “Pay with MYZ” button.
Before doing that, we had to answer a much more important question:
What is the actual source of truth for a MYZ balance?
The answer is now much clearer: one canonical MYZ ledger, shared by the internal spending flows we are building across MyZubster.
With PR #1286, we brought this architecture into Marketplace and Zorgax, removed several legacy accounting paths, and expanded the Marketplace with custom categories and a new knowledge-sharing layer.
The work has now been merged into main and deployed to production.
MYZ is an internal credit, not a currency
The first architectural decision was also the most important one.
MYZ remains an internal MyZubster utility/accounting credit.
It is not EUR.
It is not BTC.
It is not XMR.
It is not a blockchain token.
And it does not automatically represent a cash-redeemable monetary value.
That means we removed the idea of an automatic conversion such as:
10 EUR = X MYZ
or:
BTC → MYZ
EUR → MYZ
MYZ → EUR
Different payment rails can coexist, but they remain separate rails.
A product can be priced directly in MYZ:
{
"price": {
"amount": 250,
"asset": "MYZ"
}
}
or directly in EUR:
{
"price": {
"amount": 10,
"asset": "EUR"
}
}
There is no need to pretend that an exchange rate must exist between them.
One canonical MYZ ledger
The repository already had a MYZ accounting service:
src/services/myzLedgerApiService.js
The problem was that some older flows still maintained separate concepts of MYZ balance.
That could eventually lead to something like:
MYZ balance A
+
MYZ balance B
+
different update logic
no real source of truth
So we consolidated the model around the canonical ledger.
A transfer now conceptually looks like this:
payer -250 MYZ
\
> same transfer_id
/
payee +250 MYZ
The debit and credit belong to the same internal transfer.
The ledger also handles idempotency, balance validation, and protection against negative balances.
Atomic transfers and idempotency
The new central primitive is the MYZ transfer.
Conceptually:
myzLedger.transfer({
fromAccountId,
toAccountId,
amount,
transferId,
idempotencyKey,
reference
});
Every payment is associated with a transfer_id and an idempotency_key.
This matters a lot in payment systems.
An HTTP retry should never mean:
user pays once
browser retries
user pays again
It should mean:
first request → transfer created
same retry → original transfer returned
different data → idempotency conflict
We also added semantic idempotency checks.
For example, the same key cannot silently be reused to buy a different Zorgax plan.
The concurrency test that really matters
One of the most important tests is the overspending scenario.
Imagine:
Alice balance = 300 MYZ
order A = 250 MYZ
order B = 250 MYZ
both requests arrive almost at the same time
The correct result must be:
1 payment succeeds
1 payment fails
final balance = 50 MYZ
Never:
final balance = -200 MYZ
That protection should not live only in the UI.
It belongs in the accounting layer.
Marketplace: buyer → seller payments in MYZ
Marketplace listings can now be priced directly in MYZ.
When a MYZ order is paid:
buyer
↓
canonical MYZ ledger
↓
seller
The order keeps payment evidence such as:
payment.asset = MYZ
payment.network = internal-ledger
payment.status = PAID
transferId
debitEntryId
creditEntryId
We also added several protections:
only the authenticated buyer can pay;
buyer and seller cannot be the same account;
the order must be accepted before payment;
a MYZ order cannot be completed before it is PAID;
retries reuse a stable idempotency key.
Marketplace Operations now also exposes the internal MYZ balance, recent MYZ history, payment state, and transfer ID.
Zorgax can also be paid with MYZ
The same ledger is now used by Zorgax.
The conceptual flow is:
user
- MYZ ↓ zorgax:system:treasury
- MYZ ↓ entitlement
MYZ prices are not derived from the EUR price.
They are independent configuration values:
ZORGAX_PRO_PRICE_MYZ
ZORGAX_DEVELOPER_PRICE_MYZ
ZORGAX_MYZ_ACCOUNT_ID
That means we can define:
Zorgax Pro = 40 MYZ
without claiming:
40 MYZ = €9.90
Those are separate prices in separate assets.
We also chose to fail closed.
If a MYZ price has not been configured, the MYZ checkout is simply unavailable.
In production right now, the Zorgax MYZ prices are still not configured, so the system correctly reports the option as unavailable instead of inventing a conversion rate.
Removing a second legacy MYZ balance
During the work, we found an older architectural problem.
The Payment Dashboard effectively had another MYZ balance calculation based on:
canonical ledger
+
MongoDB myzCredit
utility redemptions
It also contained an older EUR → MYZ accounting conversion rule.
That would have left us with two independent MYZ accounting systems.
So we removed that behavior from the spendable balance path.
New Stripe payments no longer create MYZ automatically.
Historical rows may remain available as audit data, but they are not treated as spendable canonical MYZ.
MongoDB redemption records remain useful as fulfillment or receipt metadata, while the actual accounting movement is recorded in the canonical ledger.
The legacy Dashboard was aligned too
Some older flows still referenced Dashboard.balanceMYZ.
Those user-facing MYZ paths now read from the canonical ledger instead.
P2P MYZ transfers also go through the canonical transfer mechanism.
We also stopped presenting legacy Dashboard MYZ totals as if they represented canonical MYZ circulation.
An accounting system should not have a different source of truth depending on which screen you open.
New authenticated MYZ APIs
We added user-facing endpoints for authenticated clients:
GET /api/myz/balance
GET /api/myz/history
The MYZ account is derived from the authenticated user.
The client does not get to arbitrarily ask for someone else’s account balance.
There is also a separate service-to-service ledger API for internal operations.
Marketplace categories are now dynamic
During the same development cycle, we fixed another Marketplace limitation.
The React Marketplace previously used a mostly hard-coded list of categories.
It now loads the category catalog from:
GET /api/listings/categories
Users can also propose their own categories directly from the listing form.
The flow now looks like this:
user creates category
↓
category is pending
↓
creator can use it immediately
↓
admin review
↓
category approved
↓
globally available
This lets the Marketplace evolve with the community instead of requiring a frontend code change every time a new category is needed.
Knowledge is now a first-class Marketplace category
We also added:
knowledge
as a first-class Marketplace category.
The idea is that people should be able to share not only objects and services, but also:
know-how
guides
methods
tutorials
manuals
skills
practical knowledge
That led to a new Share Knowledge flow in the Marketplace.
Creating knowledge does not automatically create MYZ
This distinction was important to us.
When a user submits knowledge through:
POST /api/knowledge-rewards/submissions
the contribution starts as:
PENDING_REVIEW
It does not automatically create:
MYZ
a reward
a ledger entry
a transfer
scientific validation
professional validation
The contribution is stored first.
It can then be reviewed.
We intentionally keep these concepts separate:
submitted contribution
from:
reviewed contribution
from:
potential reward
They are not the same thing.
Category and knowledge moderation
We added admin review flows for both categories and knowledge.
For category proposals:
GET /api/listings/categories/proposals
PATCH /api/listings/categories/proposals/:id
For knowledge review:
PATCH /api/knowledge-rewards/submissions/:id/review
Users can also retrieve their own knowledge submissions with:
GET /api/knowledge-rewards/mine
Zorgax now understands knowledge listings too
We also updated the Zorgax listing assistant.
It can now recognize concepts such as:
knowledge
know-how
guide
tutorial
method
manual
skill
practical knowledge
and guide users toward a more appropriate Marketplace category.
It is a relatively small change, but it helps different parts of the ecosystem speak the same language.
Merge and production deployment
The main body of work was merged through:
PR #1286
feat: canonical MYZ internal payments for Marketplace and Zorgax
Merge commit:
55b0ec5545cf4172ff49fc22112c8b3d80d92f8e
Before the merge, the feature branch was also synchronized with the latest changes already landed on main.
The Vercel production deployment reached:
READY
Production aliases include:
www.myzubster.com
myzubster.com
We also ran read-only production smoke tests.
The category endpoint responds successfully and now includes knowledge.
This works as expected:
GET /api/listings?category=knowledge
The Marketplace is being served from the new production build.
No runtime error or fatal logs were observed during the production verification window.
One thing we are not claiming: “CI passed”
Transparency matters here.
The workflow:
.github/workflows/myz-ledger-contract.yml
did not generate a GitHub Actions run for either the PR head or the merge commit.
We even made a dedicated workflow-only trigger commit:
33180d233dd6abbb558ed58b56371d77eb11faa2
ci(myz): trigger canonical ledger contract
The workflow file itself is included in its push.paths.
And still:
workflow runs = 0
So we are not going to write “CI passed” when it did not.
Other GitHub Actions workflows in the repository do have execution history, which suggests this is a workflow-specific execution or configuration problem.
That remains an open follow-up.
Another important production concern: ledger persistence
The current canonical ledger implementation is file-based.
That is useful for enforcing and testing accounting invariants, but before treating MYZ writes as production-grade accounting, we need to verify one critical infrastructure requirement:
the ledger storage must be genuinely durable and consistent in production.
A successful serverless deployment does not automatically mean that a locally modified file should be treated as durable accounting storage.
So we are distinguishing:
software deployed
from:
accounting infrastructure production-ready
The code is live, but durable ledger storage still needs to be explicitly confirmed before MYZ write flows should be considered fully operational for real accounting.
Where we are now
This development cycle changed the direction of MYZ quite significantly.
The architecture is moving toward:
ONE CANONICAL MYZ LEDGER
↓
Marketplace
↓
Zorgax
↓
P2P / utilities
instead of:
Dashboard balance
Payment Dashboard balance
Marketplace logic
Zorgax logic
conversion logic
all operating independently.
At the same time, the Marketplace became more open:
dynamic categories
+
community-created categories
+
knowledge
+
submission and review workflows
What comes next
The next steps are already clear:
fix the GitHub Actions execution problem for the MYZ ledger workflow;
move or configure the canonical ledger on genuinely durable production storage;
define explicit MYZ prices for Zorgax;
decide what to do with historical legacy MYZ credits;
define identity mapping between historical contributor:* accounts and marketplace:user:* accounts;
run fully authenticated end-to-end tests once the infrastructure layer is ready.
The most important change, however, has already happened.
MYZ is no longer being treated as a number that different parts of the application can update independently.
It is becoming an internal accounting system with explicit invariants, receipts, idempotency, and a single source of truth.
And for us, that foundation matters more than any “Pay” button.
Repository: MyZubster Ecosystem
PR: #1286
Production: www.myzubster.com
Built in public. Verified where possible. Open issues documented where verification is still incomplete.
Top comments (0)