DEV Community

Daniel Ioni
Daniel Ioni

Posted on

Building Signed Wallet Requests for the MyZubster Marketplace

Building Signed Wallet Requests for the MyZubster Marketplace

We have reached an important milestone in the MyZubster Marketplace: users can now create Marketplace requests using a verified EVM wallet and an off-chain cryptographic signature.

But there is an important distinction:

Signing a Marketplace request is not the same as making a blockchain transaction.

No payment is authorized.
No gas is spent.
No transaction is sent on-chain simply because someone wants to contact a seller or request a listing.

This separation is intentional.

Here is what we built, what is already implemented, what we tested, and what still needs to happen before calling the complete flow production-ready.

The problem we wanted to solve

A Marketplace request needs stronger evidence than a simple anonymous button click.

We wanted MyZubster to be able to answer questions such as:

  • Which authenticated account created this request?
  • Which wallet controlled by that user signed it?
  • What listing was being requested?
  • What were the economic conditions at the moment of signing?
  • Has the same authorization already been used?
  • Did the listing change between review and submission?

At the same time, we did not want every Marketplace interaction to become an on-chain transaction.

That would introduce unnecessary gas, wallet friction and blockchain state for an action that is fundamentally still a request between two people.

So we separated identity, intent, payment and blockchain evidence.

The architecture

The current flow is approximately:

MyZubster account
        ↓
Connect EVM wallet
        ↓
Wallet ownership challenge
        ↓
personal_sign
        ↓
WALLET_VERIFIED
        ↓
Open Marketplace listing
        ↓
Request challenge
        ↓
Review signed economic snapshot
        ↓
personal_sign
        ↓
Backend verifies signature
        ↓
Challenge consumed atomically
        ↓
MarketplaceOrder
status: REQUESTED
Enter fullscreen mode Exit fullscreen mode

The important part is what does not happen:

Marketplace request
    ≠ payment

Marketplace signature
    ≠ blockchain transaction

Wallet verified
    ≠ seller paid

REQUESTED
    ≠ ACCEPTED
Enter fullscreen mode Exit fullscreen mode

The browser uses an EIP-1193 wallet provider and personal_sign.

It does not need to call:

eth_sendTransaction
eth_sendRawTransaction
Enter fullscreen mode Exit fullscreen mode

for the request flow.

1. EVM wallet verification

The first layer we implemented is wallet ownership verification.

The backend provides endpoints for the wallet lifecycle, including:

POST   /api/wallet/challenge
POST   /api/wallet/verify
GET    /api/wallet/me
DELETE /api/wallet/disconnect
Enter fullscreen mode Exit fullscreen mode

A user connects an EVM-compatible browser wallet.

MyZubster generates a challenge with a nonce and expiry.

The wallet signs that challenge.

The backend verifies the signature and associates the verified wallet with the authenticated MyZubster account.

The resulting states distinguish between concepts such as:

WALLET_NOT_CONNECTED
WALLET_CHALLENGE_PENDING
WALLET_VERIFIED
WALLET_DISCONNECTED
Enter fullscreen mode Exit fullscreen mode

This matters because:

CONNECTED != VERIFIED

Simply seeing a wallet address in the browser is not evidence that the authenticated MyZubster user controls it.

2. Replay-resistant challenges

Signing something once should not create a reusable authorization.

Challenge consumption was therefore hardened against replay.

A challenge has a lifecycle and cannot simply be submitted repeatedly to generate multiple Marketplace requests.

This gives us another important rule:

signature + consumed challenge
≠ reusable authorization
Enter fullscreen mode Exit fullscreen mode

Challenge expiration, previous consumption and invalid signatures are handled separately.

3. Signed Marketplace requests

After wallet verification, the user can request a real Marketplace listing.

Instead of directly creating an order, the frontend first requests a Marketplace challenge:

POST /api/marketplace/orders/challenge
Enter fullscreen mode Exit fullscreen mode

The server creates a canonical payload.

The user reviews the request and signs the server-generated message.

Only then does the client submit the signed request to:

POST /api/marketplace/orders
Enter fullscreen mode Exit fullscreen mode

The backend verifies the signature before creating the Marketplace order in the REQUESTED state.

4. Binding the signature to listing economics

This was an important hardening step.

A signature should not authorize a request based on one price while the backend silently processes another.

The V2 Marketplace request therefore includes a snapshot of the listing economics.

Conceptually:

{
  "schema": "MYZUBSTER_MARKETPLACE_REQUEST_V2",
  "listingSnapshot": {
    "price": 10,
    "currency": "EUR",
    "exchangeMode": "payment"
  }
}
Enter fullscreen mode Exit fullscreen mode

If the listing economics change after the challenge is generated but before the request is submitted, the backend rejects the request with:

MARKETPLACE_LISTING_CHANGED
Enter fullscreen mode Exit fullscreen mode

The user must review the new conditions and sign a new challenge.

That means the signature represents the conditions the user actually saw rather than merely identifying a listing ID.

5. Atomic request creation

There is another subtle problem.

Imagine this sequence:

consume challenge
↓
database error
↓
order never created
Enter fullscreen mode Exit fullscreen mode

The user would lose a valid challenge without getting the Marketplace request it was supposed to authorize.

We therefore moved signed request creation into a database transaction.

Challenge consumption and order creation are handled together.

The goal is effectively:

consume authorization
+
create REQUESTED order
=
one transactional operation
Enter fullscreen mode Exit fullscreen mode

Notifications happen after the transaction commits rather than determining whether the core request transaction succeeds.

6. The browser flow

The React Marketplace now connects the existing Request action to the signed-wallet flow.

When the user requests a listing, MyZubster can:

check authentication
↓
request wallet connection
↓
verify wallet if necessary
↓
request Marketplace challenge
↓
show conditions to the user
↓
ask for personal_sign
↓
submit signed request
Enter fullscreen mode Exit fullscreen mode

Before signing, the UI explicitly explains:

This signature creates a Marketplace request.
It does not authorize a payment.
It does not send a blockchain transaction.
No gas.

This distinction is part of the product design, not just technical documentation.

7. What we tested

The wallet/Marketplace backend checkpoint currently passes:

Test Suites: 6 passed, 6 total
Tests:       25 passed, 25 total
Enter fullscreen mode Exit fullscreen mode

The coverage includes the wallet signature service, challenge consumption, signed Marketplace request service, transactional creation, end-to-end wallet Marketplace flow and the existing Marketplace lifecycle migrated to signed requests.

We also added browser-side tests covering cases including:

  • successful signed request;
  • an already verified wallet;
  • wallet verification before the first request;
  • user rejecting the signature;
  • missing EIP-1193 wallet provider;
  • listing economics changing;
  • ensuring the request flow does not invoke an Ethereum transaction.

The React production build also completes successfully.

8. What already exists on GitHub

The implementation is now collected in the branch:

feat/wallet-marketplace-mvp
Enter fullscreen mode Exit fullscreen mode

The work consists of nine commits covering wallet verification, replay protection, signed requests, transactional creation, V2 economic snapshots, end-to-end tests and frontend integration.

It is currently under review in:

MyZubster PR #1207 — Signed Wallet Marketplace Requests

The branch deliberately does not include unrelated local verifier material.

9. Where blockchain enters the architecture

MyZubster already has a separate Marketplace evidence anchoring path targeting Base.

That is intentionally different from the buyer request signature.

The current architectural separation is:

User wallet
    ↓
off-chain signature
    ↓
Marketplace intent evidence


Marketplace lifecycle
    ↓
COMPLETED
    ↓
evidence generation
    ↓
optional blockchain anchoring
Enter fullscreen mode Exit fullscreen mode

The existing Base anchoring mechanism uses a dedicated server-side wallet for evidence anchoring.

A user's private wallet key must never be exposed to the frontend or backend.

And even when an evidence hash is anchored on-chain, we maintain another important distinction:

CONFIRMED_ON_CHAIN
!=
PHYSICAL_EVENT_VERIFIED
Enter fullscreen mode Exit fullscreen mode

A blockchain can provide evidence that a particular digital digest existed and was anchored.

It does not magically prove that a physical product existed, that 100 kg of material was recycled, that a service was actually performed, or that an environmental claim is true.

Those claims require their own evidence.

What is still missing?

Passing tests is not the same as being production-ready.

There are several remaining steps.

Production deployment and public E2E verification

The new branch still needs to move through the normal review/merge/deployment process.

After deployment we need a real public end-to-end verification using the production HTTPS environment and a compatible wallet.

Stronger domain binding

The wallet challenge can be strengthened further with SIWE-style domain and URI binding.

That would make the context in which a signature is requested even more explicit.

Rate limiting

Wallet challenge and verification endpoints should receive production-grade rate limiting and abuse controls.

More idempotency guarantees

The signed request architecture already has replay protection and transactional challenge consumption, but additional database-level uniqueness around evidence/challenge identifiers can provide another defensive layer.

Money canonicalization

Before real payments become part of this flow, monetary values should use a strict canonical representation, such as integer minor units where appropriate, instead of relying on ambiguous decimal representations.

Payments

The signed request system deliberately does not implement automatic payment.

MyZubster's Marketplace direction remains free-first:

Create account
↓
SELLER_FREE
↓
Publish
↓
Receive requests
↓
Build Marketplace activity
↓
Activate payments only when actually needed
Enter fullscreen mode Exit fullscreen mode

Payment onboarding must remain a separate, explicit action.

No silent subscription and no automatic charge should happen because a user connected a wallet or reached a listing threshold.

Seller payment infrastructure

Later phases still need to define and implement the production payment lifecycle:

NOT_CONFIGURED
→ ONBOARDING
→ REVIEW
→ READY
→ RESTRICTED / DISABLED
Enter fullscreen mode Exit fullscreen mode

This includes payment-provider onboarding, payouts, refunds, disputes, webhooks, compliance boundaries and transparent Marketplace fees.

Zorgax Listing Assistant

Another planned Marketplace layer is Zorgax-assisted listing creation.

The idea is:

Seller starts listing
↓
Zorgax asks structured questions
↓
draft generated
↓
seller reviews/edit
↓
explicit confirmation
↓
publish
Enter fullscreen mode Exit fullscreen mode

Zorgax should assist with fields such as title, category, description, price or FREE/BARTER mode, location, availability, characteristics and other structured listing information.

But evidence-first rules remain important:

ZORGAX_DRAFT
!=
SELLER_CONFIRMED
!=
PUBLISHED
Enter fullscreen mode Exit fullscreen mode

Zorgax should help structure information, not invent factual claims.

Why this architecture matters

The interesting part for us is not simply "adding a crypto wallet to a Marketplace."

The objective is to connect several independent layers without pretending they prove the same thing:

Account identity
        ↓
Wallet ownership
        ↓
Signed intent
        ↓
Marketplace state
        ↓
Payment state
        ↓
Digital evidence
        ↓
Optional blockchain anchor
Enter fullscreen mode Exit fullscreen mode

Each transition should have its own evidence.

That gives us a much more useful architecture than putting every interaction on-chain.

Blockchain is used where immutable digital anchoring can add value.

Cryptographic signatures are used where user authorization and intent need evidence.

Traditional database transactions are used where application consistency matters.

Payments remain their own explicit financial operation.

And claims about the physical world remain separate from all of them.

Current status

At this milestone:

EVM wallet linking             IMPLEMENTED
Wallet ownership verification  IMPLEMENTED
Challenge replay protection    IMPLEMENTED
Signed Marketplace requests    IMPLEMENTED
V2 economic snapshot           IMPLEMENTED
Transactional request creation IMPLEMENTED
React wallet request flow      IMPLEMENTED
Backend checkpoint             25/25 PASS
React production build         PASS

Public production deployment   PENDING
Public wallet E2E              PENDING
SIWE-style domain binding      PENDING
Production rate limiting       PENDING
Real payment onboarding        FUTURE PHASE
Zorgax Listing Assistant       NEXT MARKETPLACE PHASE
Enter fullscreen mode Exit fullscreen mode

The goal is not to put everything on a blockchain.

The goal is to make it clear what was signed, what was paid, what was recorded, what was anchored, and what has actually been verified.

That is the direction we are building toward with MyZubster.


MyZubster is being developed openly.

PR #1207 contains the current signed-wallet Marketplace implementation and its test coverage.

Feedback on the architecture, especially around wallet UX, signature-domain binding, replay protection and evidence design, is welcome.

Top comments (0)