DEV Community

Daniel Ioni
Daniel Ioni

Posted on

Building MyZubster’s Internal MYZ Payments, Marketplace Knowledge Layer, and a Safer Ledger

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)