DEV Community

Daniel Ioni
Daniel Ioni

Posted on

From “Connect MetaMask” to Verifiable Marketplace Intent: What We Built in MyZubster and What Comes Next

From “Connect MetaMask” to Verifiable Marketplace Intent: What We Built in MyZubster and What Comes Next

I’ve been working on a new Ethereum / MetaMask layer for MyZubster.

What started as:

“Let’s add MetaMask.”

quickly became a much more interesting architecture problem.

Because adding a wallet button is easy.

Defining what a wallet means inside a real application is not.

Over the latest development cycle, I implemented four connected pieces:

MyZubster account
↓
verified MetaMask wallet
↓
optional Ethereum login
↓
signed Marketplace request
↓
Seller-visible cryptographic evidence

But one principle has guided the entire implementation:

CONNECTED
!= VERIFIED
!= AUTHENTICATED
!= REQUEST_SIGNED
!= ACCEPTED
!= PAID
!= SETTLED

Keeping those states separate is probably the most important part of the work.

Why I didn’t want “Web3 everything”

One possible approach would have been:

User opens MyZubster
↓
Connect wallet
↓
Every important action becomes a blockchain transaction
↓
Pay gas
↓
Wait for confirmations

I deliberately did not take that route.

MyZubster is not intended to become unusable for people who do not already live inside the crypto ecosystem.

So MetaMask remains an additional capability.

Users can still access MyZubster through traditional authentication methods.

The emerging identity model is:

Email / password
Google
GitHub
Facebook
MetaMask
↓
MyZubster account

But the wallet adds cryptographic capabilities when they are actually useful.

Milestone 1: proving wallet ownership

The first problem was simple to describe:

How does MyZubster know that the authenticated user really controls a particular Ethereum address?

Reading an address from MetaMask is not sufficient.

This:

const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});

only proves that the browser exposed an account.

It does not give the backend a trustworthy ownership proof.

So the first implementation introduced a challenge-signature flow.

The architecture is:

Authenticated MyZubster user
↓
Connect MetaMask
↓
eth_requestAccounts
↓
read chain ID
↓
POST /api/wallet/challenge
↓
server-generated nonce + challenge
↓
personal_sign
↓
POST /api/wallet/verify
↓
ethers.verifyMessage()
↓
recovered signer == expected wallet
↓
WALLET_VERIFIED

The backend, not the frontend, decides whether a wallet is verified.

The challenge

The challenge is tied to:

MyZubster account
wallet address
chain ID
domain
application URI
random nonce
issued time
expiration
LINK_WALLET action

And the message explicitly tells the user:

This is not a payment and does not spend ETH.

That sentence is more important than it might appear.

Wallet UX should not blur the difference between:

sign message

and:

send transaction
What MyZubster does not store

The wallet integration is non-custodial.

MyZubster does not need:

private keys
seed phrases
mnemonics
MetaMask passwords
wallet secrets

The application stores only the relationship needed to represent verified wallet ownership.

Conceptually:

evmWallet: {
walletType: 'EVM',
provider: 'metamask',
address: '0x...',
chainId: 1,
status: 'WALLET_VERIFIED',
verifiedAt: Date,
lastVerifiedAt: Date
}

The user remains in control of the wallet.

Milestone 2: signing Marketplace intent

Once wallet ownership existed, the next question became:

Can the buyer cryptographically prove that they intended to request a particular Marketplace listing?

Without paying gas?

Yes.

That became the second milestone.

Instead of broadcasting an Ethereum transaction, MyZubster creates a canonical off-chain request.

The flow is now:

Verified MyZubster wallet
↓
Buyer clicks Request
↓
POST /api/marketplace/orders/challenge
↓
canonical MARKETPLACE_REQUEST generated
↓
MetaMask signs it
↓
backend verifies signer
↓
MarketplaceOrder created
↓
status = REQUESTED

No ETH is transferred.

No gas is required.

What exactly gets signed?

The server builds a structured request containing information such as:

{
"schema": "myzubster.marketplace-request.v1",
"intent": "MARKETPLACE_REQUEST",
"listingId": "...",
"quantity": 1,
"buyerAccountReference": "...",
"walletAddress": "0x...",
"chainId": 1,
"listingSnapshot": {
"title": "...",
"price": 25,
"currency": "EUR",
"exchangeMode": "payment",
"sellerId": "..."
},
"nonce": "...",
"issuedAt": "...",
"expiresAt": "..."
}

The listing snapshot is important.

The signature should represent what the buyer actually saw and requested.

It should not silently become authorization for a listing after its commercial terms have changed.

Replay protection

A signed payload should not be reusable forever.

The Marketplace challenge therefore includes:

random nonce
expiration
user binding
wallet binding
listing binding
quantity binding

Once successfully used, the challenge becomes consumed.

A second attempt is rejected.

Conceptually:

challenge
↓
signed
↓
verified
↓
order created
↓
CONSUMED

Not:

same signature
↓
order 1
↓
order 2
↓
order 3
What gets stored on the Marketplace order?

The order now has a dedicated wallet evidence layer.

Conceptually:

walletEvidence: {
status: 'VERIFIED',
walletAddress: '0x...',
networkFamily: 'EVM',
chainId: 1,
payloadHash: '...',
challengeId: '...',
requestSchema: 'myzubster.marketplace-request.v1',
signedAt: Date,
verifiedAt: Date
}

The raw signature exists as evidence but is hidden from normal Mongoose queries.

The important public-facing evidence is the verified relationship and cryptographic hash, not dumping internal authentication material everywhere.

Milestone 3: Sign-In with Ethereum

After wallet ownership verification, another possibility became available.

If a MyZubster account already has a verified EVM wallet, that wallet can also become an optional authentication method.

So I added an Ethereum login path.

The login page can now conceptually support:

Email + password
Google
GitHub
Facebook
MetaMask

MetaMask does not replace the existing authentication system.

It extends it.

Ethereum login flow

The flow is:

/social-login
↓
Continue with MetaMask
↓
eth_requestAccounts
↓
eth_chainId
↓
POST /api/auth/ethereum/challenge
↓
short-lived SIWE-style challenge
↓
personal_sign
↓
POST /api/auth/ethereum/verify
↓
server recovers Ethereum signer
↓
find existing MyZubster account with verified wallet
↓
issue normal MyZubster JWT

The result is still a normal MyZubster session.

MetaMask is the authentication proof.

It does not mean the rest of MyZubster suddenly needs blockchain calls.

An intentional limitation: no automatic wallet-only account creation yet

For the first version, a random Ethereum wallet cannot silently create a brand-new MyZubster account.

The wallet must already be linked to an existing account.

So:

unknown wallet
+
valid Ethereum signature

does not automatically become:

new MyZubster user

Instead, the user is asked to first enter through an existing identity method and link the wallet.

That is intentional.

Wallet-only onboarding introduces additional questions:

How does account recovery work?

What happens if the wallet is lost?

How do we attach an email later?

Can wallets be rotated?

Can an account have multiple wallets?

How do support and abuse recovery work?

I want those questions answered before wallet-only registration becomes possible.

Milestone 4: showing the proof to the Seller

Cryptographic evidence is not very useful if nobody can understand it.

So the next step was the Seller experience.

When a Seller receives a Marketplace request that was signed using a verified wallet, MyZubster now displays:

✓ Richiesta firmata · wallet verificato

The Seller can see information such as:

buyer wallet
EVM chain ID
request payload hash
verification time

The address and hash are abbreviated in the interface, while the complete values remain available where useful.

What the Seller does not see

I deliberately introduced an API projection instead of blindly returning the stored evidence object.

The participant-facing endpoint returns only information required to understand the proof.

It does not expose:

raw MetaMask signature
internal challenge ID
private challenge state

This is a small implementation detail, but it represents an important rule:

cryptographic evidence does not mean every internal cryptographic artifact should automatically become public.

Seller UX: signed is not paid

The evidence panel also explicitly says:

This signature proves request intent
and wallet control.

It is not a payment.
It does not transfer ETH.
It does not mean the Seller accepted the request.
It does not mean the order is settled.

This distinction is essential to the architecture.

Today the lifecycle looks like:

Wallet verified
↓
Request signed
↓
REQUESTED
↓
Seller accepts
↓
ACCEPTED
↓
payment, if required
↓
payment verification
↓
COMPLETED

Signing step 2 does not magically jump to step 6.

Why I think this architecture is more useful than “put everything on-chain”

A Marketplace contains many actions.

Examples:

browse listing
request item
ask question
accept request
reject request
cancel
review
moderate
pay
settle
record evidence

Putting every one of those on-chain would introduce enormous friction.

Instead, MyZubster can use different evidence layers for different actions.

For example:

ordinary application action
→ database

important user intent
→ signed off-chain payload

payment
→ payment rail

permanent public evidence
→ optional blockchain anchor

That is much closer to how I think blockchain integration should work.

Use cryptography where cryptography adds something.

Use blockchain where permanence adds something.

Don’t turn every button into gas.

The current PR stack

The implementation is currently divided into several pull requests.

PR #1385 — MetaMask wallet ownership linking

Introduces:

Connect MetaMask
wallet challenge
signature verification
verified EVM wallet state
disconnect flow
PR #1386 — Signed MetaMask Marketplace requests

Introduces:

MARKETPLACE_REQUEST challenge
canonical request payload
off-chain signature
replay protection
walletEvidence on MarketplaceOrder
PR #1387 — Optional Sign-In with Ethereum

Introduces:

Continue with MetaMask
Ethereum authentication challenge
signature verification
existing account lookup
normal MyZubster JWT
PR #1388 — Seller-visible wallet evidence

Introduces:

verified request badge
wallet address
chain ID
payload hash
verification timestamp
safe API projection
signed-intent vs payment explanation

So the architecture is now becoming:

ACCOUNT
↓
WALLET OWNERSHIP
↓
AUTHENTICATION
↓
SIGNED INTENT
↓
MARKETPLACE STATE
↓
PAYMENT
↓
SETTLEMENT
↓
OPTIONAL PERMANENT EVIDENCE
What is still missing?

Quite a lot.

And I think documenting the unfinished parts is as important as documenting what works.

  1. Merge and production validation

The wallet work currently exists as a stack of development pull requests.

Before calling the system production-ready, I still need to complete:

CI validation
security checks
merge ordering
deployment
production smoke tests
real MetaMask browser tests

Passing unit tests is not the same as validating the complete browser-wallet-server journey.

  1. Real Seller acceptance evidence

The buyer can sign a request.

The Seller currently accepts it through the normal authenticated application flow.

A future version could optionally introduce:

SELLER_ACCEPTANCE

as a separately signed intent.

For example:

buyer:
MARKETPLACE_REQUEST signed

seller:
MARKETPLACE_ACCEPTANCE signed

That could produce bilateral evidence.

But this should remain optional rather than forcing both parties through wallet signatures for every exchange.

  1. ETH payment flow

The biggest missing layer is still actual Ethereum payment.

The architecture should eventually distinguish:

PAYMENT_REQUESTED
PAYMENT_SUBMITTED
PAYMENT_CONFIRMING
PAID
SETTLED
FAILED

A buyer signature is not payment evidence.

Actual ETH payment needs transaction-level evidence.

That means checking things like:

network
transaction hash
sender
recipient
amount
block inclusion
confirmation count
transaction status

before saying:

PAID

  1. Network policy

MetaMask can be connected to many EVM networks.

MyZubster therefore needs an explicit network policy.

For example:

Ethereum Mainnet
Ethereum Sepolia
Base
Base Sepolia
future EVM chains

Wallet ownership itself can be chain-agnostic in some contexts.

Payments cannot.

If an order expects ETH on Ethereum mainnet, a transaction on another chain must not satisfy it.

  1. Wallet switching

Another real-world UX problem:

A user verifies:

0xAAA

but later MetaMask is currently using:

0xBBB

The frontend already detects some wallet mismatch situations, but this needs stronger product handling.

Eventually MyZubster should clearly show:

Verified wallet:
0xAAA

Currently selected MetaMask account:
0xBBB

Switch account to continue.

  1. Wallet recovery and rotation

This becomes important once MetaMask can authenticate users.

What happens if somebody loses a wallet?

The application needs a recovery model.

Possible future flow:

authenticate through another verified MyZubster identity
↓
request wallet replacement
↓
security confirmation
↓
old wallet revoked
↓
new wallet challenge
↓
new wallet verified

Without that, wallet authentication risks becoming a trap for users who lose access.

  1. Multiple wallets

Today the model is essentially:

MyZubster account
↓
one verified EVM wallet

Long term, a user might reasonably need:

primary wallet
Marketplace wallet
personal wallet
project wallet
DAO wallet

That would require a proper wallet registry rather than a single wallet property.

  1. Standard SIWE compatibility

The Ethereum authentication message is currently SIWE-style.

A future improvement is stricter compatibility with the formal Sign-In with Ethereum model and dedicated parsing/validation instead of relying only on the application’s own message construction.

That would improve interoperability and reduce ambiguity.

  1. Mobile wallet support

window.ethereum works well for browser-extension environments such as desktop MetaMask.

That is not enough for everyone.

Mobile support may eventually require:

MetaMask mobile deeplinks
WalletConnect
other EIP-1193 compatible providers

I don’t want the feature to become “MetaMask desktop only”.

  1. Better evidence inspection

The Seller currently gets a compact evidence summary.

A future interface could provide an expandable verification view:

Request schema
Payload hash
Wallet
Network
Signed at
Verified at
Listing snapshot
Verification result

without exposing secrets.

Potentially something like:

Verify signed request

where MyZubster independently recomputes the payload hash.

  1. Payment evidence must remain separate

This needs to remain a permanent architectural rule:

wallet verified
!= payment verified

Even if the wallet that signed the request later sends the payment.

Those are two distinct cryptographic events.

Request evidence says:

this wallet expressed intent.

Payment evidence says:

this blockchain transaction moved value according to the expected payment conditions.

They should never be collapsed into one status.

  1. Blockchain anchoring

Once an exchange is completed, MyZubster already has experiments around evidence anchoring.

Eventually the complete evidence chain could look like:

buyer wallet proof
↓
signed request
↓
seller acceptance
↓
payment evidence
↓
completed Marketplace order
↓
canonical completion evidence
↓
optional Base / Ethereum anchor

Again, blockchain anchoring should be optional and evidence-driven.

A Marketplace order should never be described as “on-chain” simply because the application supports blockchain somewhere else.

What I learned from this implementation

The interesting lesson is that integrating MetaMask is not really about MetaMask.

The JavaScript call itself is tiny.

window.ethereum.request(...)

The real engineering work is defining what a signature means.

For every wallet action, the application has to answer:

Who is signing?

What exactly are they signing?

Why are they signing it?

How long is that signature valid?

Can it be replayed?

What account is it bound to?

What does the signature authorize?

What does it explicitly NOT authorize?

What evidence should another participant see?

What must remain private?

That is where the real architecture lives.

Where MyZubster is heading

The broader idea is becoming clearer.

MyZubster does not need one giant identity system or one giant blockchain transaction system.

It can combine several independently verifiable layers:

MyZubster identity
GitHub identity
Ethereum wallet control
signed intent
Marketplace lifecycle
payment evidence
community reputation
Zorgax assistance
optional blockchain permanence

Each layer has its own meaning.

And Zorgax can eventually help users understand those meanings rather than pretending everything is the same kind of “verification”.

Current architecture

Today, the direction is:

Human
↓
MyZubster account
↓
optional verified identities
├── GitHub
├── Google
└── Ethereum wallet
↓
signed action
↓
application state
↓
human counterparty decision
↓
payment evidence
↓
optional blockchain evidence

That feels much more useful to me than:

Connect wallet
↓
Web3
Next development milestone

The next major technical milestone is the payment boundary:

SIGNED REQUEST
↓
SELLER ACCEPTANCE
↓
ETH PAYMENT REQUEST
↓
MetaMask transaction approval
↓
Ethereum transaction
↓
server-side verification
↓
confirmations
↓
PAID

Only there should MyZubster start talking about actual ETH movement.

Until that point:

A signature is evidence of intent, not evidence of payment.

And I want that distinction to remain visible in both the code and the product.

Project: MyZubster
Stack: Node.js, Express, MongoDB, React, ethers, MetaMask
Architecture: non-custodial wallet verification + off-chain signed intent
Status: active development / PR validation
Next major milestone: verified ETH payment lifecycle

Repository:

https://github.com/MyZubster-Ecosystem/myzubster

Current implementation PRs:

1385 — MetaMask wallet ownership

1386 — signed Marketplace requests

1387 — optional Sign-In with Ethereum

1388 — Seller-visible wallet evidence

DEV.to tags

Top comments (0)