<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged sepolia</title>
<link>https://coderlegion.com/tag/sepolia</link>
<description>Powered by Question2Answer</description>
<item>
<title>🚀 MyZubster: Completing a Real Ethereum Sepolia Marketplace E2E Payment Flow</title>
<link>https://coderlegion.com/28746/myzubster-completing-a-real-ethereum-sepolia-marketplace-e2e-payment-flow</link>
<description>&lt;p&gt;MyZubster Sepolia E2E — Technical Deep Dive: Debugging the Full Marketplace Payment Pipeline&lt;br&gt;
I have just completed a full end-to-end technical validation of the MyZubster Marketplace ETH payment pipeline on Ethereum Sepolia.&lt;br&gt;
This was not a simple frontend test.&lt;br&gt;
The work involved debugging and validating multiple layers of the system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React routing&lt;/li&gt;
&lt;li&gt;React development server&lt;/li&gt;
&lt;li&gt;Webpack Dev Server&lt;/li&gt;
&lt;li&gt;browser runtime diagnostics&lt;/li&gt;
&lt;li&gt;frontend bundle generation&lt;/li&gt;
&lt;li&gt;Cloudflare CDN caching&lt;/li&gt;
&lt;li&gt;Cloudflare Tunnel ingress rules&lt;/li&gt;
&lt;li&gt;SPA fallback behavior&lt;/li&gt;
&lt;li&gt;API/backend routing&lt;/li&gt;
&lt;li&gt;authentication/JWT handling&lt;/li&gt;
&lt;li&gt;Marketplace order state&lt;/li&gt;
&lt;li&gt;payment-intent generation&lt;/li&gt;
&lt;li&gt;MetaMask&lt;/li&gt;
&lt;li&gt;Ethereum Sepolia&lt;/li&gt;
&lt;li&gt;transaction construction&lt;/li&gt;
&lt;li&gt;on-chain transaction verification&lt;/li&gt;
&lt;li&gt;confirmation tracking&lt;/li&gt;
&lt;li&gt;final Marketplace payment state&lt;br&gt;
The interesting part was that several apparently unrelated browser errors were actually symptoms of infrastructure and routing problems.&lt;ol&gt;
&lt;li&gt;Starting point: the Marketplace Operations route&lt;br&gt;
The target route was:&lt;br&gt;
MyZubster Marketplace Operations — Sepolia&lt;br&gt;
The React application explicitly defines:&lt;br&gt;
if (path === '/marketplace/scambi') {&lt;br&gt;
return ;&lt;br&gt;
}&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;and the navigation contains:&lt;br&gt;
window.location.href = '/marketplace/scambi';&lt;/p&gt;
&lt;p&gt;So the expected application architecture was:&lt;br&gt;
/marketplace&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MarketplacePage&lt;/p&gt;
&lt;p&gt;/marketplace/scambi&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MarketplaceOpsPage&lt;/p&gt;
&lt;p&gt;The first task was proving that this code was actually reaching the browser.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Frontend development server investigation&lt;br&gt;
The frontend was running through Create React App:&lt;br&gt;
react-scripts start&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The relevant process was:&lt;br&gt;
/usr/bin/node&lt;br&gt;
/root/myzubster-sepolia-e2e/frontend/node_modules/react-scripts/scripts/start.js&lt;/p&gt;
&lt;p&gt;The application was bound to:&lt;br&gt;
127.0.0.1:3010&lt;/p&gt;
&lt;p&gt;We verified the listener with:&lt;br&gt;
ss -ltnp | grep ':3010'&lt;/p&gt;
&lt;p&gt;which returned the React process.&lt;br&gt;
The process tree was also inspected:&lt;br&gt;
sh&lt;br&gt;
 └── react-scripts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  └── node
       └── webpack/react development server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was important because an attempted restart initially produced:&lt;br&gt;
Something is already running on port 3010.&lt;/p&gt;
&lt;p&gt;The issue was not that React could not start.&lt;br&gt;
A previous React development process was already occupying the port.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Local route versus public route&lt;br&gt;
We then compared:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;http://127.0.0.1:3010/marketplace/scambi&quot;&gt;http://127.0.0.1:3010/marketplace/scambi&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;against:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://sepolia.myzubster.com/marketplace/scambi&quot;&gt;https://sepolia.myzubster.com/marketplace/scambi&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Both returned:&lt;br&gt;
HTTP 200&lt;br&gt;
Content-Type: text/html&lt;/p&gt;
&lt;p&gt;This initially suggested that the route was fine.&lt;br&gt;
But an HTTP 200 response alone does not prove that the correct React application version is being served.&lt;br&gt;
The next step was therefore to inspect the actual JavaScript bundle.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Runtime instrumentation&lt;br&gt;
A temporary diagnostic marker was inserted into App.js:&lt;br&gt;
console.log('[MYZ-APP-RUNTIME]', {&lt;br&gt;
  pathname: window.location.pathname,&lt;br&gt;
  path,&lt;br&gt;
  marketplaceOps: path === '/marketplace/scambi',&lt;br&gt;
  href: window.location.href&lt;br&gt;
});&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The browser then reported:&lt;br&gt;
pathname: &quot;/marketplace/scambi&quot;&lt;br&gt;
path: &quot;/marketplace/scambi&quot;&lt;br&gt;
marketplaceOps: true&lt;/p&gt;
&lt;p&gt;This established that the browser itself was correctly interpreting:&lt;br&gt;
/marketplace/scambi&lt;/p&gt;
&lt;p&gt;as the Marketplace Operations route.&lt;br&gt;
We also inspected the DOM and confirmed that the browser was loading:&lt;br&gt;
/static/js/bundle.js&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bundle-level verification&lt;br&gt;
The bundle was downloaded and searched directly.&lt;br&gt;
The relevant route was found in the compiled JavaScript:&lt;br&gt;
if (path === '/marketplace/scambi')&lt;br&gt;
  return MarketplaceOpsPage;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We also searched for:&lt;br&gt;
Paga ETH su Sepolia&lt;/p&gt;
&lt;p&gt;and found it.&lt;br&gt;
This was important because it proved that the Marketplace Operations component was not merely present in the source tree.&lt;br&gt;
It was present in the bundle being served.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cloudflare cache investigation&lt;br&gt;
The next layer was Cloudflare.&lt;br&gt;
The public bundle returned headers such as:&lt;br&gt;
HTTP/2 200&lt;br&gt;
content-type: application/javascript; charset=utf-8&lt;br&gt;
etag: W/&quot;2c9e23-...&quot;&lt;br&gt;
age: 582&lt;br&gt;
cache-control: max-age=14400&lt;br&gt;
cf-cache-status: HIT&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The local bundle had a different ETag:&lt;br&gt;
ETag: W/&quot;2c9ec3-...&quot;&lt;/p&gt;
&lt;p&gt;This was the key observation.&lt;br&gt;
The local development server had a newer bundle, while Cloudflare was still serving an older cached object.&lt;br&gt;
So the situation was:&lt;br&gt;
SOURCE CODE&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React build/dev server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NEW BUNDLE&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;│
X
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cloudflare cache&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OLD BUNDLE&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Browser&lt;/p&gt;
&lt;p&gt;This can be particularly confusing because:&lt;br&gt;
HTTP 200&lt;/p&gt;
&lt;p&gt;does not mean:&lt;br&gt;
latest application version&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Targeted Cloudflare cache purge&lt;br&gt;
Instead of performing a full cache purge, the specific asset was targeted:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://sepolia.myzubster.com/static/js/bundle.js&quot;&gt;https://sepolia.myzubster.com/static/js/bundle.js&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;through Cloudflare's cache purge interface.&lt;br&gt;
Cloudflare Dashboard&lt;br&gt;
After the purge, a cache-busting request returned:&lt;br&gt;
cf-cache-status: MISS&lt;/p&gt;
&lt;p&gt;and the public ETag changed to the current bundle's ETag.&lt;br&gt;
This established that Cloudflare was now fetching the updated asset from the origin.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WebSocket error from Webpack Dev Server&lt;br&gt;
The browser was also reporting:&lt;br&gt;
Firefox cannot establish a connection to&lt;br&gt;
ws://127.0.0.1:3010/ws&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This initially looked alarming because the public site was being accessed through:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://sepolia.myzubster.com&quot;&gt;https://sepolia.myzubster.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;while the Webpack development client was attempting to establish a WebSocket against:&lt;br&gt;
127.0.0.1:3010&lt;/p&gt;
&lt;p&gt;The important distinction is that this is development-server HMR/WebSocket infrastructure, not the Marketplace HTTP/API request itself.&lt;br&gt;
The application bundle was nevertheless loading and executing.&lt;br&gt;
The browser continued to report:&lt;br&gt;
MYZ-APP-RUNTIME&lt;/p&gt;
&lt;p&gt;and:&lt;br&gt;
marketplaceOps: true&lt;/p&gt;
&lt;p&gt;so the WebSocket failure was not the root cause of the Marketplace API problem.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The real API problem&lt;br&gt;
The more significant error appeared when testing:&lt;br&gt;
/api/marketplace/orders/mine&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The browser expected JSON.&lt;br&gt;
Instead, it received:&lt;br&gt;
HTTP 200&lt;br&gt;
Content-Type: text/html&lt;/p&gt;
&lt;p&gt;with:&lt;br&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;This immediately explained:&lt;br&gt;
JSON.parse:&lt;br&gt;
unexpected character at line 1 column 1&lt;/p&gt;
&lt;p&gt;The frontend wasn't receiving malformed JSON.&lt;br&gt;
It wasn't receiving JSON at all.&lt;br&gt;
It was receiving the React application's HTML.&lt;br&gt;
This is a classic SPA/API routing failure.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why this happened&lt;br&gt;
The Cloudflare Tunnel configuration originally routed:&lt;br&gt;
sepolia.myzubster.com&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;127.0.0.1:3010&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;But the backend API was actually listening on:&lt;br&gt;
127.0.0.1:5010&lt;/p&gt;
&lt;p&gt;Therefore:&lt;br&gt;
GET /api/marketplace/orders/mine&lt;/p&gt;
&lt;p&gt;was effectively becoming:&lt;br&gt;
Cloudflare&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Frontend :3010&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React index.html&lt;/p&gt;
&lt;p&gt;instead of:&lt;br&gt;
Cloudflare&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backend :5010&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON API&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Correcting Cloudflare ingress&lt;br&gt;
The ingress configuration was changed to explicitly separate API traffic:&lt;ul&gt;
&lt;li&gt;hostname: sepolia.myzubster.com&lt;br&gt;
path: /api/*&lt;br&gt;
service: &lt;a rel=&quot;nofollow&quot; href=&quot;http://127.0.0.1:5010&quot;&gt;http://127.0.0.1:5010&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;hostname: sepolia.myzubster.com&lt;br&gt;
service: &lt;a rel=&quot;nofollow&quot; href=&quot;http://127.0.0.1:3010&quot;&gt;http://127.0.0.1:3010&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The routing architecture became:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                     Cloudflare Tunnel
                            │
              sepolia.myzubster.com
                            │
                ┌───────────┴───────────┐
                │                       │
             /api/*                   /*
                │                       │
                ▼                       ▼
           Backend :5010          Frontend :3010
                │                       │
                ▼                       ▼
              JSON                  React SPA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The configuration was validated:&lt;br&gt;
Validating rules from /root/.cloudflared/config.yml&lt;br&gt;
OK&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Discovering how Cloudflared was actually managed&lt;br&gt;
An attempt to restart:&lt;br&gt;
systemctl restart cloudflared&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;failed because:&lt;br&gt;
cloudflared.service not found&lt;/p&gt;
&lt;p&gt;We then traced the process:&lt;br&gt;
systemd&lt;br&gt;
 └── PM2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  └── cloudflared
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PM2 showed:&lt;br&gt;
id: 4&lt;br&gt;
name: cloudflared&lt;br&gt;
status: online&lt;/p&gt;
&lt;p&gt;and the command:&lt;br&gt;
/usr/local/bin/cloudflared tunnel&lt;br&gt;
--config /root/.cloudflared/config.yml&lt;br&gt;
--protocol http2&lt;br&gt;
run myzubster&lt;/p&gt;
&lt;p&gt;Therefore the correct restart mechanism was:&lt;br&gt;
pm2 restart 4&lt;/p&gt;
&lt;p&gt;or:&lt;br&gt;
pm2 restart cloudflared&lt;/p&gt;
&lt;p&gt;This is another useful operational lesson: before restarting infrastructure, identify which process manager actually owns it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;API routing verification&lt;br&gt;
After the Tunnel configuration was corrected, the same public endpoint:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://sepolia.myzubster.com/api/marketplace/orders/mine&quot;&gt;https://sepolia.myzubster.com/api/marketplace/orders/mine&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;returned:&lt;br&gt;
HTTP/2 401&lt;br&gt;
Content-Type: application/json&lt;/p&gt;
&lt;p&gt;with:&lt;br&gt;
{&lt;br&gt;
  &quot;success&quot;: false,&lt;br&gt;
  &quot;message&quot;: &quot;Token di autenticazione mancante o non valido&quot;&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;This was exactly what we wanted from an unauthenticated request.&lt;br&gt;
The important change was:&lt;br&gt;
Before&lt;br&gt;
200&lt;br&gt;
text/html&lt;br&gt;
React index.html&lt;/p&gt;
&lt;p&gt;After&lt;br&gt;
401&lt;br&gt;
application/json&lt;br&gt;
Backend authentication response&lt;/p&gt;
&lt;p&gt;A 401 here was actually evidence that routing was fixed.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Authenticated Marketplace order&lt;br&gt;
Using the authenticated browser session:&lt;br&gt;
fetch('/api/marketplace/orders/mine', {&lt;br&gt;
  headers: {&lt;br&gt;
Authorization:&lt;br&gt;
  &lt;code&gt;Bearer ${localStorage.getItem('myzubster-token')}&lt;/code&gt;&lt;br&gt;
  },&lt;br&gt;
  cache: 'no-store'&lt;br&gt;
})&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;the backend returned the actual test order.&lt;br&gt;
The order was:&lt;br&gt;
ID:&lt;br&gt;
6ab870ffc33da1e61579745d&lt;/p&gt;
&lt;p&gt;with:&lt;br&gt;
status: ACCEPTED&lt;/p&gt;
&lt;p&gt;and:&lt;br&gt;
payment.status: AWAITING_PAYMENT&lt;/p&gt;
&lt;p&gt;The order snapshot contained:&lt;br&gt;
title:&lt;br&gt;
MyZubster Sepolia ETH E2E Test&lt;/p&gt;
&lt;p&gt;price:&lt;br&gt;
0.0001&lt;/p&gt;
&lt;p&gt;currency:&lt;br&gt;
ETH&lt;/p&gt;
&lt;p&gt;exchangeMode:&lt;br&gt;
payment&lt;/p&gt;
&lt;p&gt;At this stage the Marketplace order existed correctly, but the blockchain payment had not yet been made.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Payment-intent endpoint debugging&lt;br&gt;
The frontend initially attempted:&lt;br&gt;
/api/marketplace/orders/{id}/payment-intent&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;which returned:&lt;br&gt;
404&lt;br&gt;
Cannot GET /api/marketplace/orders/.../payment-intent&lt;/p&gt;
&lt;p&gt;The actual endpoint was:&lt;br&gt;
/api/marketplace/orders/{id}/payment/eth-intent&lt;/p&gt;
&lt;p&gt;For the E2E order:&lt;br&gt;
/api/marketplace/orders/6ab870ffc33da1e61579745d/payment/eth-intent&lt;/p&gt;
&lt;p&gt;the backend returned:&lt;br&gt;
HTTP 200&lt;br&gt;
Content-Type: application/json&lt;/p&gt;
&lt;p&gt;with the complete payment intent.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Payment intent structure&lt;br&gt;
The backend returned:&lt;br&gt;
{&lt;br&gt;
  &quot;success&quot;: true,&lt;br&gt;
  &quot;data&quot;: {&lt;br&gt;
&quot;asset&quot;: &quot;ETH&quot;,&lt;br&gt;
&quot;network&quot;: &quot;sepolia&quot;,&lt;br&gt;
&quot;chainId&quot;: 11155111,&lt;br&gt;
&quot;chainHex&quot;: &quot;0xaa36a7&quot;,&lt;br&gt;
&quot;expectedSender&quot;: &quot;0xB15707286c4dFC6f642fcDf217419b2665e1847a&quot;,&lt;br&gt;
&quot;expectedRecipient&quot;: &quot;0x14d751a54E8617C5A9DF87972dC980827fA91e4d&quot;,&lt;br&gt;
&quot;expectedAmountWei&quot;: &quot;100000000000000&quot;,&lt;br&gt;
&quot;expectedAmountEth&quot;: &quot;0.0001&quot;,&lt;br&gt;
&quot;minConfirmations&quot;: 3,&lt;br&gt;
&quot;paymentStatus&quot;: &quot;AWAITING_PAYMENT&quot;,&lt;br&gt;
&quot;txId&quot;: null,&lt;br&gt;
&quot;testnet&quot;: true&lt;br&gt;
  }&lt;br&gt;
}&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This establishes several critical properties.&lt;br&gt;
Network&lt;br&gt;
Sepolia&lt;/p&gt;
&lt;p&gt;Chain&lt;br&gt;
11155111&lt;/p&gt;
&lt;p&gt;Amount&lt;br&gt;
100000000000000 wei&lt;/p&gt;
&lt;p&gt;which corresponds to:&lt;br&gt;
0.0001 ETH&lt;/p&gt;
&lt;p&gt;Required confirmation depth&lt;br&gt;
3&lt;/p&gt;
&lt;p&gt;Expected sender&lt;br&gt;
0xB15707286c4dFC6f642fcDf217419b2665e1847a&lt;/p&gt;
&lt;p&gt;Expected recipient&lt;br&gt;
0x14d751a54E8617C5A9DF87972dC980827fA91e4d&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;MetaMask and testnet funding&lt;br&gt;
When attempting to execute the transaction, MetaMask correctly identified that the wallet did not initially have enough SepoliaETH to cover the transaction and network fees.&lt;br&gt;
This is an important distinction in E2E testing:&lt;br&gt;
Payment amount&lt;br&gt;
+&lt;br&gt;
Gas fee&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;both require testnet funds.&lt;br&gt;
The test wallet was subsequently funded using a Sepolia faucet.&lt;br&gt;
Once the wallet had sufficient SepoliaETH, the transaction could be submitted.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Transaction execution&lt;br&gt;
MetaMask was used to submit:&lt;br&gt;
0.0001 SepoliaETH&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;from:&lt;br&gt;
0xB15707286c4dFC6f642fcDf217419b2665e1847a&lt;/p&gt;
&lt;p&gt;to:&lt;br&gt;
0x14d751a54E8617C5A9DF87972dC980827fA91e4d&lt;/p&gt;
&lt;p&gt;The transaction was submitted on:&lt;br&gt;
Ethereum Sepolia&lt;br&gt;
Chain ID: 11155111&lt;/p&gt;
&lt;p&gt;The Marketplace immediately entered:&lt;br&gt;
PAYMENT: CONFIRMING&lt;/p&gt;
&lt;p&gt;rather than incorrectly assuming that the payment was already final.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Confirmation tracking&lt;br&gt;
The UI displayed:&lt;br&gt;
Confirmations: 0&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;immediately after transaction submission.&lt;br&gt;
This is an important state distinction:&lt;br&gt;
transaction submitted&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ≠
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;payment finalized&lt;/p&gt;
&lt;p&gt;The backend continued checking the blockchain until the required confirmation depth was reached.&lt;br&gt;
The configured threshold was:&lt;br&gt;
3 confirmations&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Server-side verification&lt;br&gt;
The final payment state was not determined solely by the frontend or by MetaMask.&lt;br&gt;
The verification process used the payment intent as the expected source of truth.&lt;br&gt;
Conceptually:&lt;br&gt;
Expected sender&lt;br&gt;
   │&lt;br&gt;
   ├───────────────┐&lt;br&gt;
Expected recipient    │&lt;br&gt;
   │              │&lt;br&gt;
Expected amount       │&lt;br&gt;
   │              ▼&lt;br&gt;
Expected network → Blockchain transaction&lt;pre&gt;&lt;code&gt;              │
              ▼
         Verification
              │
       ┌──────┴──────┐
       │             │
    Valid?       Confirmations
       │             │
       └──────┬──────┘
              ▼
             PAID
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The final transaction satisfied the expected payment parameters.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Final transaction state&lt;br&gt;
The Marketplace eventually showed:&lt;br&gt;
PAYMENT: PAID&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;with:&lt;br&gt;
Amount:&lt;br&gt;
0.0001 ETH&lt;/p&gt;
&lt;p&gt;Network:&lt;br&gt;
Ethereum Sepolia&lt;/p&gt;
&lt;p&gt;Confirmations:&lt;br&gt;
3&lt;/p&gt;
&lt;p&gt;and the transaction evidence:&lt;br&gt;
TX:&lt;br&gt;
0xae0b...5af9f94c37&lt;/p&gt;
&lt;p&gt;The final sender and recipient matched the payment intent.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Complete state transition&lt;br&gt;
The entire Marketplace payment state machine was demonstrated:&lt;br&gt;
┌──────────────────────┐&lt;br&gt;
│       ACCEPTED       │&lt;br&gt;
└──────────┬───────────┘&lt;pre&gt;&lt;code&gt;   │
   ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;┌──────────────────────┐&lt;br&gt;
│  AWAITING_PAYMENT    │&lt;br&gt;
└──────────┬───────────┘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   │
   │ MetaMask transaction
   ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;┌──────────────────────┐&lt;br&gt;
│     CONFIRMING       │&lt;br&gt;
│   confirmations: 0   │&lt;br&gt;
└──────────┬───────────┘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   │
   │ blockchain
   │ confirmations
   ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;┌──────────────────────┐&lt;br&gt;
│     CONFIRMING       │&lt;br&gt;
│   confirmations: 3   │&lt;br&gt;
└──────────┬───────────┘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   │
   ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;┌──────────────────────┐&lt;br&gt;
│         PAID         │&lt;br&gt;
└──────────────────────┘&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is substantially more meaningful than a mocked frontend payment.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Problems encountered and resolved&lt;br&gt;
The E2E test exposed several independent technical problems.&lt;br&gt;
Problem 1 — Existing React process&lt;br&gt;
Something is already running on port 3010&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Resolution: identify the existing react-scripts process and manage the correct process.&lt;br&gt;
Problem 2 — Stale Cloudflare bundle&lt;br&gt;
cf-cache-status: HIT&lt;br&gt;
age: 582&lt;/p&gt;
&lt;p&gt;with an outdated ETag.&lt;br&gt;
Resolution: targeted Cloudflare cache purge.&lt;br&gt;
Problem 3 — Webpack development WebSocket&lt;br&gt;
ws://127.0.0.1:3010/ws&lt;/p&gt;
&lt;p&gt;could not be reached from the public browser.&lt;br&gt;
Resolution: identified as development-server/HMR infrastructure rather than the Marketplace API failure.&lt;br&gt;
Problem 4 — API returned HTML&lt;br&gt;
/api/marketplace/orders/mine&lt;/p&gt;
&lt;p&gt;returned:&lt;br&gt;
200 text/html&lt;/p&gt;
&lt;p&gt;instead of JSON.&lt;br&gt;
Resolution: separate /api/* from frontend routing in Cloudflare Tunnel.&lt;br&gt;
Problem 5 — Wrong payment-intent endpoint&lt;br&gt;
The frontend attempted:&lt;br&gt;
/payment-intent&lt;/p&gt;
&lt;p&gt;while the backend exposed:&lt;br&gt;
/payment/eth-intent&lt;/p&gt;
&lt;p&gt;Resolution: use the correct endpoint.&lt;br&gt;
Problem 6 — Missing SepoliaETH&lt;br&gt;
MetaMask reported insufficient funds for network fees.&lt;br&gt;
Resolution: fund the Sepolia test wallet through a faucet.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What makes this a real E2E test?&lt;br&gt;
A useful distinction is:&lt;br&gt;
Mocked test&lt;br&gt;
Click Pay&lt;br&gt;
   ↓&lt;br&gt;
setState(PAID)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;versus:&lt;br&gt;
This test&lt;br&gt;
Marketplace order&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;authenticated API&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;payment intent&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MetaMask&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;real Sepolia transaction&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ethereum network&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;transaction confirmation&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;server-side verification&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3 confirmations&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PAID&lt;/p&gt;
&lt;p&gt;The second flow crosses actual system boundaries.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Infrastructure architecture after the fixes&lt;br&gt;
The final architecture is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                 INTERNET
                    │
                    ▼
             Cloudflare Edge
                    │
                    ▼
          Cloudflare Tunnel
                    │
     ┌──────────────┴──────────────┐
     │                             │
 /api/*                         /*
     │                             │
     ▼                             ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ┌──────────────┐             ┌──────────────┐&lt;br&gt;
  │ Backend      │             │ React        │&lt;br&gt;
  │ 127.0.0.1    │             │ 127.0.0.1    │&lt;br&gt;
  │ :5010        │             │ :3010        │&lt;br&gt;
  └──────┬───────┘             └──────┬───────┘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     │                            │
     ▼                            ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  Marketplace API               Marketplace UI&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     │
     ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  Payment Intent&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     │
     ▼
  MetaMask
     │
     ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Ethereum Sepolia&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     │
     ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;   Blockchain TX&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     │
     ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  Server Verification&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     │
     ▼
    PAID
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Useful debugging methodology&lt;br&gt;
One of the biggest takeaways from this test is that debugging a Web3 application is not limited to the blockchain.&lt;br&gt;
The actual dependency chain was:&lt;br&gt;
Browser&lt;br&gt;
 ↓&lt;br&gt;
React&lt;br&gt;
 ↓&lt;br&gt;
Webpack&lt;br&gt;
 ↓&lt;br&gt;
HTTP&lt;br&gt;
 ↓&lt;br&gt;
Cloudflare&lt;br&gt;
 ↓&lt;br&gt;
Tunnel&lt;br&gt;
 ↓&lt;br&gt;
Backend&lt;br&gt;
 ↓&lt;br&gt;
JWT&lt;br&gt;
 ↓&lt;br&gt;
Marketplace&lt;br&gt;
 ↓&lt;br&gt;
Payment intent&lt;br&gt;
 ↓&lt;br&gt;
MetaMask&lt;br&gt;
 ↓&lt;br&gt;
Ethereum&lt;br&gt;
 ↓&lt;br&gt;
Verification&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A failure anywhere in this chain can manifest as a completely misleading browser error.&lt;br&gt;
For example:&lt;br&gt;
JSON.parse error&lt;/p&gt;
&lt;p&gt;looked like a frontend parsing bug.&lt;br&gt;
The real problem was:&lt;br&gt;
Cloudflare routing&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React index.html&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON.parse(&quot;&amp;lt;!DOCTYPE html&amp;gt;&quot;)&lt;/p&gt;
&lt;p&gt;Likewise:&lt;br&gt;
HTTP 200&lt;/p&gt;
&lt;p&gt;looked successful.&lt;br&gt;
But:&lt;br&gt;
200 + text/html&lt;/p&gt;
&lt;p&gt;was actually evidence of a routing failure.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Live demonstration&lt;br&gt;
The complete Marketplace Operations interface is available here:&lt;br&gt;
MyZubster Marketplace Operations — Sepolia&lt;br&gt;
The public Sepolia environment:&lt;br&gt;
MyZubster Sepolia Environment&lt;br&gt;
The blockchain can be independently inspected through:&lt;br&gt;
Ethereum Sepolia Etherscan&lt;br&gt;
The completed transaction can be located using the transaction hash displayed by the Marketplace payment verification panel:&lt;br&gt;
0xae0b...5af9f94c37&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Final technical checklist&lt;br&gt;
[✓] React frontend running&lt;br&gt;
[✓] Marketplace route detected&lt;br&gt;
[✓] MarketplaceOpsPage present in bundle&lt;br&gt;
[✓] Runtime diagnostics verified&lt;br&gt;
[✓] Public bundle verified&lt;br&gt;
[✓] Cloudflare stale cache identified&lt;br&gt;
[✓] Cloudflare cache purged&lt;br&gt;
[✓] Frontend served current bundle&lt;br&gt;
[✓] API routing issue identified&lt;br&gt;
[✓] /api/* routed to backend :5010&lt;br&gt;
[✓] Frontend routes remain on :3010&lt;br&gt;
[✓] Cloudflare Tunnel configuration validated&lt;br&gt;
[✓] PM2 cloudflared process identified&lt;br&gt;
[✓] cloudflared restarted correctly&lt;br&gt;
[✓] Unauthenticated API returns JSON 401&lt;br&gt;
[✓] Authenticated orders endpoint works&lt;br&gt;
[✓] Marketplace order retrieved&lt;br&gt;
[✓] ETH payment intent generated&lt;br&gt;
[✓] Sepolia chain ID verified&lt;br&gt;
[✓] Sender verified&lt;br&gt;
[✓] Recipient verified&lt;br&gt;
[✓] Amount verified&lt;br&gt;
[✓] MetaMask transaction executed&lt;br&gt;
[✓] Sepolia transaction confirmed&lt;br&gt;
[✓] Server-side verification completed&lt;br&gt;
[✓] 3 confirmations reached&lt;br&gt;
[✓] Payment transitioned to PAID&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Conclusion&lt;br&gt;
The most important result of this work is not simply that 0.0001 SepoliaETH was transferred.&lt;br&gt;
The important result is that the entire distributed system was exercised and debugged across its real boundaries.&lt;br&gt;
We started with frontend routing and a browser that was not behaving as expected.&lt;br&gt;
We then traced the problem through:&lt;br&gt;
React&lt;br&gt;
→ Webpack Dev Server&lt;br&gt;
→ browser bundle&lt;br&gt;
→ Cloudflare cache&lt;br&gt;
→ Cloudflare Tunnel&lt;br&gt;
→ API routing&lt;br&gt;
→ backend authentication&lt;br&gt;
→ Marketplace order&lt;br&gt;
→ payment intent&lt;br&gt;
→ MetaMask&lt;br&gt;
→ Ethereum Sepolia&lt;br&gt;
→ blockchain confirmations&lt;br&gt;
→ server-side verification&lt;/p&gt;
&lt;p&gt;The final state was:&lt;br&gt;
Marketplace Order&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    │
    ▼
  ACCEPTED
    │
    ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Payment Intent&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    │
    ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; MetaMask / Sepolia&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    │
    ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Blockchain TX&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    │
    ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 3 Confirmations&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    │
    ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Server Verification&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    │
    ▼
   PAID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The MyZubster Sepolia ETH Marketplace payment pipeline has therefore been successfully exercised end-to-end against a real Sepolia blockchain transaction.&lt;/p&gt;
&lt;h2 id=&quot;myzubster-web3-ethereum-sepolia-blockchain-react-nodejs-express-metamask-cloudflare-cloudflaretunnel-devops-e2e-integrationtesting-api-jwt-opensource-marketplace-paymentinfrastructure-softwareengineering&quot;&gt;MyZubster #Web3 #Ethereum #Sepolia #Blockchain #React #NodeJS #Express #MetaMask #Cloudflare #CloudflareTunnel #DevOps #E2E #IntegrationTesting #API #JWT #OpenSource #Marketplace #PaymentInfrastructure #SoftwareEngineering&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28746/myzubster-completing-a-real-ethereum-sepolia-marketplace-e2e-payment-flow</guid>
<pubDate>Sun, 27 Sep 2026 02:50:20 +0000</pubDate>
</item>
<item>
<title>Debugging 6.5 Million PM2 Restarts: Stabilizing MyZubster Sepolia and I-ECO-01</title>
<link>https://coderlegion.com/28722/debugging-6-5-million-pm2-restarts-stabilizing-myzubster-sepolia-and-i-eco-01</link>
<description>&lt;h2 id=&quot;debugging-6-5-million-pm2-restarts-stabilizing-myzubster-sepolia-and-i-eco-01&quot;&gt;Debugging 6.5 Million PM2 Restarts: Stabilizing MyZubster Sepolia and I-ECO-01&lt;/h2&gt;
&lt;p&gt;Today I completed a significant infrastructure debugging session across the &lt;strong&gt;MyZubster Sepolia E2E environment&lt;/strong&gt; and the &lt;strong&gt;I-ECO-01 / UrbanLab backend&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;What initially looked like a few unrelated configuration problems turned into a useful example of why production debugging needs to happen layer by layer.&lt;/p&gt;
&lt;p&gt;The work involved &lt;strong&gt;Node.js, PM2, MongoDB, Google OAuth 2.0, Cloudflare, Socket.IO, and a Linux VPS&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here is what happened, what was broken, and how it was fixed.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-google-oauth-was-reaching-the-backend-but-authentication-still-failed&quot;&gt;1. Google OAuth Was Reaching the Backend — But Authentication Still Failed&lt;/h3&gt;
&lt;p&gt;The first problem appeared in the Google social-login flow.&lt;/p&gt;
&lt;p&gt;The authorization request itself looked correct.&lt;/p&gt;
&lt;p&gt;Google was receiving the expected client ID and redirect URI:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://sepolia.myzubster.com/api/auth/social/google/callback&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The callback also reached the backend correctly.&lt;/p&gt;
&lt;p&gt;Debug instrumentation confirmed:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hasCode: true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hasState: true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hasError: false&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;So the browser redirect, public route, OAuth state and callback routing were all working.&lt;/p&gt;
&lt;p&gt;The failure happened one step later: during the authorization-code exchange.&lt;/p&gt;
&lt;p&gt;The backend logged:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;401 invalid_client&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and Google reported that the provided client secret was invalid.&lt;/p&gt;
&lt;p&gt;That was an important distinction.&lt;/p&gt;
&lt;p&gt;The problem was not the redirect URI.&lt;/p&gt;
&lt;p&gt;It was not Cloudflare.&lt;/p&gt;
&lt;p&gt;It was not Express routing.&lt;/p&gt;
&lt;p&gt;It was not the OAuth state.&lt;/p&gt;
&lt;p&gt;It was not the frontend.&lt;/p&gt;
&lt;p&gt;The OAuth client credential itself was wrong.&lt;/p&gt;
&lt;p&gt;After correcting the configuration, the backend reached:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OAUTH-CALLBACK-ENTER&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;followed by:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OAUTH-CALLBACK-SUCCESS&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The server-side Google OAuth flow was working.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-the-env-file-was-correct-the-running-process-was-not&quot;&gt;2. The &lt;code&gt;.env&lt;/code&gt; File Was Correct — The Running Process Was Not&lt;/h3&gt;
&lt;p&gt;The next issue was more subtle.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file contained:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FRONTEND_URL=https://sepolia.myzubster.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GATEWAY_PUBLIC_URL=https://sepolia.myzubster.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Yet after successful OAuth authentication, the application could still redirect toward an old Vercel deployment.&lt;/p&gt;
&lt;p&gt;This highlighted an important operational detail:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Changing &lt;code&gt;.env&lt;/code&gt; does not automatically mean a long-running process is using the new values.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I inspected the configuration at several levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Node + dotenv&lt;/li&gt;
&lt;li&gt;PM2 process configuration&lt;/li&gt;
&lt;li&gt;actual process environment&lt;/li&gt;
&lt;li&gt;OAuth redirect generation&lt;/li&gt;
&lt;li&gt;backend logs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The active PM2 environment was then aligned with the Sepolia deployment:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FRONTEND_URL=https://sepolia.myzubster.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PUBLIC_APP_URL=https://sepolia.myzubster.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GATEWAY_PUBLIC_URL=https://sepolia.myzubster.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;After that, the active process environment was verified directly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-then-i-found-a-process-with-more-than-6-5-million-restarts&quot;&gt;3. Then I Found a Process With More Than 6.5 Million Restarts&lt;/h3&gt;
&lt;p&gt;While checking the rest of the VPS, another issue appeared.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;UrbanLab / I-ECO-01&lt;/strong&gt; PM2 process was shown as online, but its restart counter was enormous:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6,548,384 restarts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That immediately suggested a crash/restart loop.&lt;/p&gt;
&lt;p&gt;This is one of the reasons I do not treat:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PM2 status = online&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;as proof that an application is healthy.&lt;/p&gt;
&lt;p&gt;A process can start, crash immediately, restart automatically and still repeatedly appear online when inspected at the wrong moment.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-finding-the-root-cause&quot;&gt;4. Finding the Root Cause&lt;/h3&gt;
&lt;p&gt;PM2 was executing:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/opt/I-ECO-01/server.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I inspected the server entrypoint and found these dependencies:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dotenv&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;express&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cors&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;helmet&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mongoose&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;socket.io&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;winston&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then I compared those requirements against &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The project only declared:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cors&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dotenv&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;express&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mongoose&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The logs confirmed the immediate failure:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Error: Cannot find module 'helmet'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The server crashed at:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const helmet = require('helmet');&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Because PM2 automatically restarted the process, the cycle repeated continuously.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-fixing-the-dependency-graph&quot;&gt;5. Fixing the Dependency Graph&lt;/h3&gt;
&lt;p&gt;Instead of installing only the first missing package and waiting for the next crash, I inspected all external imports in the server entrypoint.&lt;/p&gt;
&lt;p&gt;Three npm dependencies were missing:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;helmet&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;socket.io&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;winston&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http&lt;/code&gt; did not require installation because it is part of Node.js itself.&lt;/p&gt;
&lt;p&gt;The missing dependencies were installed and persisted to &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The resulting application dependencies now include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Express&lt;/li&gt;
&lt;li&gt;CORS&lt;/li&gt;
&lt;li&gt;dotenv&lt;/li&gt;
&lt;li&gt;Helmet&lt;/li&gt;
&lt;li&gt;Mongoose&lt;/li&gt;
&lt;li&gt;Socket.IO&lt;/li&gt;
&lt;li&gt;Winston&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each new module was then tested independently with &lt;code&gt;require()&lt;/code&gt; before restarting the service.&lt;/p&gt;
&lt;p&gt;All modules loaded successfully.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-application-startup-was-not-enough-i-tested-stability&quot;&gt;6. Application Startup Was Not Enough — I Tested Stability&lt;/h3&gt;
&lt;p&gt;After restarting UrbanLab, the application successfully initialized its Escrow controller.&lt;/p&gt;
&lt;p&gt;The server reported:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Server started on port 5002&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;MongoDB connected successfully.&lt;/p&gt;
&lt;p&gt;The health endpoint:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://127.0.0.1:5002/health&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;returned:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;status: ok&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;service: I-ECO-01&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mongodb: connected&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;But I still did not consider the problem solved.&lt;/p&gt;
&lt;p&gt;The original issue was a restart loop, so the correct test was to verify process stability over time.&lt;/p&gt;
&lt;p&gt;Before the stability test:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PID = 1026452&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;restarts = 6548384&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;After 15 seconds:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PID = 1026452&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;restarts = 6548384&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Same PID.&lt;/p&gt;
&lt;p&gt;Same restart counter.&lt;/p&gt;
&lt;p&gt;The loop had stopped.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-escrow-infrastructure-is-running-again&quot;&gt;7. Escrow Infrastructure Is Running Again&lt;/h3&gt;
&lt;p&gt;Once the dependency problem was resolved, the I-ECO-01 Escrow controller initialized with:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Threshold:&lt;/strong&gt; 2/3&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Timeout:&lt;/strong&gt; 24 hours&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fee:&lt;/strong&gt; 0.5%&lt;/p&gt;
&lt;p&gt;The service also initialized its HTTP API and WebSocket infrastructure.&lt;/p&gt;
&lt;p&gt;MongoDB remained connected and the health endpoint continued responding successfully.&lt;/p&gt;
&lt;p&gt;So the service moved from an uncontrolled crash loop to an operational backend.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-old-logs-can-be-misleading&quot;&gt;8. Old Logs Can Be Misleading&lt;/h3&gt;
&lt;p&gt;One interesting detail appeared after the repair.&lt;/p&gt;
&lt;p&gt;Running:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pm2 logs urbanlab&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;still displayed:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cannot find module 'helmet'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;At first glance, that could suggest that the problem still existed.&lt;/p&gt;
&lt;p&gt;But those entries were historical.&lt;/p&gt;
&lt;p&gt;The current process had already successfully loaded Helmet, initialized the application, connected to MongoDB and remained alive with the same PID.&lt;/p&gt;
&lt;p&gt;This is why timestamps, process IDs, restart counters and health checks matter when reading operational logs.&lt;/p&gt;
&lt;p&gt;A log file can contain a real error that is no longer a current error.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-what-i-verified&quot;&gt;9. What I Verified&lt;/h3&gt;
&lt;p&gt;By the end of the session:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Google OAuth provider:&lt;/strong&gt; operational&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Google OAuth callback:&lt;/strong&gt; operational&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OAuth authorization-code exchange:&lt;/strong&gt; successful&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OAuth state handling:&lt;/strong&gt; verified&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sepolia public URL configuration:&lt;/strong&gt; corrected&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PM2 runtime environment:&lt;/strong&gt; verified&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I-ECO-01 Node.js dependencies:&lt;/strong&gt; repaired&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UrbanLab restart loop:&lt;/strong&gt; stopped&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MongoDB:&lt;/strong&gt; connected&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Health endpoint:&lt;/strong&gt; healthy&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Escrow controller:&lt;/strong&gt; initialized&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2-of-3 multisig configuration:&lt;/strong&gt; loaded&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Socket.IO:&lt;/strong&gt; initialized&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PM2/systemd environment:&lt;/strong&gt; verified&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;10-the-main-lessons&quot;&gt;10. The Main Lessons&lt;/h3&gt;
&lt;p&gt;This debugging session reinforced several useful principles.&lt;/p&gt;
&lt;h4 id=&quot;online-does-not-mean-healthy&quot;&gt;&lt;code&gt;online&lt;/code&gt; does not mean healthy&lt;/h4&gt;
&lt;p&gt;A process manager tells you about process state.&lt;/p&gt;
&lt;p&gt;An application health endpoint tells you about application state.&lt;/p&gt;
&lt;p&gt;You often need both.&lt;/p&gt;
&lt;h4 id=&quot;-env-does-not-equal-runtime-environment&quot;&gt;&lt;code&gt;.env&lt;/code&gt; does not equal runtime environment&lt;/h4&gt;
&lt;p&gt;Always verify what the running process actually sees.&lt;/p&gt;
&lt;p&gt;This is especially important when using PM2, containers, systemd or other long-running process managers.&lt;/p&gt;
&lt;h4 id=&quot;debug-oauth-one-stage-at-a-time&quot;&gt;Debug OAuth one stage at a time&lt;/h4&gt;
&lt;p&gt;OAuth involves multiple independent stages:&lt;/p&gt;
&lt;p&gt;authorization request → provider consent → callback → state verification → authorization-code exchange → profile retrieval → application session → frontend redirect.&lt;/p&gt;
&lt;p&gt;Logging each boundary makes failures much easier to isolate.&lt;/p&gt;
&lt;h4 id=&quot;fix-dependency-problems-systematically&quot;&gt;Fix dependency problems systematically&lt;/h4&gt;
&lt;p&gt;When one &lt;code&gt;require()&lt;/code&gt; fails, inspect the entire entrypoint instead of repeatedly installing dependencies one crash at a time.&lt;/p&gt;
&lt;h4 id=&quot;verify-the-original-failure-condition&quot;&gt;Verify the original failure condition&lt;/h4&gt;
&lt;p&gt;The problem was a restart loop.&lt;/p&gt;
&lt;p&gt;Therefore, successful startup alone was not enough.&lt;/p&gt;
&lt;p&gt;The actual success condition was:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;same PID + unchanged restart counter + healthy endpoint over time.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-result&quot;&gt;Final Result&lt;/h3&gt;
&lt;p&gt;The MyZubster Sepolia environment is now significantly more stable.&lt;/p&gt;
&lt;p&gt;Google OAuth can successfully complete its backend authentication flow.&lt;/p&gt;
&lt;p&gt;The Sepolia runtime URLs are correctly propagated to the active backend process.&lt;/p&gt;
&lt;p&gt;I-ECO-01 is connected to MongoDB.&lt;/p&gt;
&lt;p&gt;The Escrow service is initialized.&lt;/p&gt;
&lt;p&gt;And, most importantly, a Node.js service that had accumulated more than &lt;strong&gt;6.5 million PM2 restarts&lt;/strong&gt; is now staying alive.&lt;/p&gt;
&lt;p&gt;Sometimes the most useful development work is not adding another feature.&lt;/p&gt;
&lt;p&gt;It is understanding exactly why the existing system is failing — and proving that the fix actually removed the failure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Node.js · Express · MongoDB · PM2 · Google OAuth 2.0 · Socket.IO · Cloudflare · Linux · Sepolia&lt;/p&gt;
&lt;h2 id=&quot;nodejs-devops-webdevelopment-backenddevelopment-debugging-pm2-mongodb-oauth2-googleoauth-socketio-web3-sepolia-cloudflare-linux-myzubster-coderlegion&quot;&gt;NodeJS #DevOps #WebDevelopment #BackendDevelopment #Debugging #PM2 #MongoDB #OAuth2 #GoogleOAuth #SocketIO #Web3 #Sepolia #Cloudflare #Linux #MyZubster #CoderLegion&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28722/debugging-6-5-million-pm2-restarts-stabilizing-myzubster-sepolia-and-i-eco-01</guid>
<pubDate>Sat, 26 Sep 2026 14:17:31 +0000</pubDate>
</item>
<item>
<title>Building a Real MetaMask + Ethereum Sepolia Payment Flow for MyZubster</title>
<link>https://coderlegion.com/28576/building-a-real-metamask-ethereum-sepolia-payment-flow-for-myzubster</link>
<description>&lt;p&gt;Building a Real MetaMask + Ethereum Sepolia Payment Flow for MyZubster&lt;/p&gt;
&lt;p&gt;I’ve been working on the next stage of the MyZubster Marketplace: connecting authenticated users, MetaMask wallets, signed marketplace requests, and real blockchain payment verification.&lt;/p&gt;
&lt;p&gt;The target flow is:&lt;/p&gt;
&lt;p&gt;MyZubster account&lt;br&gt;
→ verified MetaMask wallet&lt;br&gt;
→ signed marketplace request&lt;br&gt;
→ seller acceptance&lt;br&gt;
→ Ethereum Sepolia payment&lt;br&gt;
→ server-side verification&lt;br&gt;
→ PAID&lt;br&gt;
→ COMPLETED&lt;/p&gt;
&lt;p&gt;This week we reached an important milestone: real wallet ownership verification is working end-to-end.&lt;/p&gt;
&lt;p&gt;What is working now&lt;/p&gt;
&lt;p&gt;We created a completely isolated E2E environment on the VPS instead of experimenting directly in production.&lt;/p&gt;
&lt;p&gt;The test stack currently runs as:&lt;/p&gt;
&lt;p&gt;React frontend&lt;br&gt;
127.0.0.1:3010&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓ proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MyZubster backend&lt;br&gt;
127.0.0.1:5010&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MongoDB + Ethereum Sepolia RPC&lt;/p&gt;
&lt;p&gt;The environment stays private. Instead of exposing the development ports publicly, I access the React frontend from my local machine using an SSH tunnel.&lt;/p&gt;
&lt;p&gt;That also means I can use the real MetaMask browser extension against the remote test backend.&lt;/p&gt;
&lt;p&gt;Real MetaMask verification&lt;/p&gt;
&lt;p&gt;The first working flow is:&lt;/p&gt;
&lt;p&gt;Authenticated MyZubster user&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Connect MetaMask&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backend generates challenge&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MetaMask personal_sign&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backend verifies signature&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wallet becomes VERIFIED&lt;/p&gt;
&lt;p&gt;And this is now working with a real wallet.&lt;/p&gt;
&lt;p&gt;The UI successfully reached:&lt;/p&gt;
&lt;p&gt;Verified · 0xBBBc51B471…03f514bCBb&lt;/p&gt;
&lt;p&gt;with confirmation that the wallet was linked to the MyZubster account.&lt;/p&gt;
&lt;p&gt;The important detail is that this is not a payment.&lt;/p&gt;
&lt;p&gt;personal_sign is only being used to prove wallet ownership.&lt;/p&gt;
&lt;p&gt;No ETH is transferred and no gas is consumed.&lt;/p&gt;
&lt;p&gt;Authentication and wallet ownership are separate&lt;/p&gt;
&lt;p&gt;One thing I wanted to preserve in the architecture is a strict distinction between platform identity and blockchain identity.&lt;/p&gt;
&lt;p&gt;A user first authenticates with MyZubster, currently through mechanisms such as Google or GitHub OAuth.&lt;/p&gt;
&lt;p&gt;MetaMask then proves control of an EVM address.&lt;/p&gt;
&lt;p&gt;So:&lt;/p&gt;
&lt;p&gt;AUTHENTICATED ≠ WALLET VERIFIED&lt;/p&gt;
&lt;p&gt;And later:&lt;/p&gt;
&lt;p&gt;WALLET VERIFIED&lt;br&gt;
≠ REQUEST SIGNED&lt;br&gt;
≠ TX SUBMITTED&lt;br&gt;
≠ PAID&lt;br&gt;
≠ COMPLETED&lt;/p&gt;
&lt;p&gt;Those states should never collapse into a single “connected” status.&lt;/p&gt;
&lt;p&gt;An interesting network detail&lt;/p&gt;
&lt;p&gt;The wallet verification test happened while MetaMask was on:&lt;/p&gt;
&lt;p&gt;chainId 84532&lt;/p&gt;
&lt;p&gt;which is Base Sepolia.&lt;/p&gt;
&lt;p&gt;The Marketplace payment system itself targets:&lt;/p&gt;
&lt;p&gt;Ethereum Sepolia&lt;br&gt;
chainId 11155111&lt;br&gt;
0xaa36a7&lt;/p&gt;
&lt;p&gt;That is intentional.&lt;/p&gt;
&lt;p&gt;Wallet ownership verification proves control of an EVM address.&lt;/p&gt;
&lt;p&gt;The actual payment step independently checks the required network and can request MetaMask to switch to Ethereum Sepolia before sending anything.&lt;/p&gt;
&lt;p&gt;Signed Marketplace requests&lt;/p&gt;
&lt;p&gt;The next layer is already implemented.&lt;/p&gt;
&lt;p&gt;When a Buyer requests a real Marketplace listing, MyZubster can generate an order-specific challenge.&lt;/p&gt;
&lt;p&gt;The Buyer signs it with MetaMask:&lt;/p&gt;
&lt;p&gt;Marketplace listing&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Order challenge&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;personal_sign&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Signature verification&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Signed request&lt;/p&gt;
&lt;p&gt;Again, this does not move funds.&lt;/p&gt;
&lt;p&gt;It creates cryptographic evidence that the verified Buyer wallet approved that specific request.&lt;/p&gt;
&lt;p&gt;The Seller can then see wallet evidence before deciding whether to accept the order.&lt;/p&gt;
&lt;p&gt;The payment architecture&lt;/p&gt;
&lt;p&gt;After Seller acceptance, the server creates an ETH payment intent.&lt;/p&gt;
&lt;p&gt;That intent defines the expected:&lt;/p&gt;
&lt;p&gt;sender&lt;br&gt;
recipient&lt;br&gt;
amount&lt;br&gt;
chain&lt;/p&gt;
&lt;p&gt;The frontend then verifies that the currently selected MetaMask account matches the Buyer wallet.&lt;/p&gt;
&lt;p&gt;It also verifies Ethereum Sepolia.&lt;/p&gt;
&lt;p&gt;Only then can it call:&lt;/p&gt;
&lt;p&gt;ethereum.request({&lt;br&gt;
  method: &quot;eth_sendTransaction&quot;,&lt;br&gt;
  ...&lt;br&gt;
})&lt;/p&gt;
&lt;p&gt;MetaMask still requires explicit approval from the user.&lt;/p&gt;
&lt;p&gt;MyZubster never receives the private key.&lt;/p&gt;
&lt;p&gt;A transaction hash is not proof of payment&lt;/p&gt;
&lt;p&gt;This is probably the part of the architecture I care most about.&lt;/p&gt;
&lt;p&gt;The browser submitting a transaction hash does not automatically change the order to PAID.&lt;/p&gt;
&lt;p&gt;The backend independently checks the blockchain through the Sepolia RPC.&lt;/p&gt;
&lt;p&gt;It verifies things such as:&lt;/p&gt;
&lt;p&gt;correct sender&lt;br&gt;
correct recipient&lt;br&gt;
correct network&lt;br&gt;
correct amount&lt;br&gt;
successful transaction&lt;br&gt;
minimum confirmations&lt;/p&gt;
&lt;p&gt;The lifecycle can therefore look like:&lt;/p&gt;
&lt;p&gt;TX_SUBMITTED&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CONFIRMING&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3 confirmations&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PAID&lt;/p&gt;
&lt;p&gt;If something does not match, the verifier can reject the transaction instead.&lt;/p&gt;
&lt;p&gt;What has actually been tested&lt;/p&gt;
&lt;p&gt;So far the real environment has validated the isolated VPS deployment, frontend/backend proxy, Ethereum Sepolia RPC access, SSH browser access, OAuth-authenticated MyZubster session, MetaMask detection, wallet connection, real personal_sign, server-side signature verification, and successful wallet linking.&lt;/p&gt;
&lt;p&gt;The blockchain payment code exists, but I am deliberately not calling the whole Marketplace payment flow complete yet.&lt;/p&gt;
&lt;p&gt;What remains&lt;/p&gt;
&lt;p&gt;The next E2E test needs two actors:&lt;/p&gt;
&lt;p&gt;Seller account + Seller MetaMask wallet&lt;/p&gt;
&lt;p&gt;Buyer account + Buyer MetaMask wallet&lt;/p&gt;
&lt;p&gt;The Seller will create an ETH listing.&lt;/p&gt;
&lt;p&gt;The Buyer will sign a request.&lt;/p&gt;
&lt;p&gt;The Seller will inspect and accept it.&lt;/p&gt;
&lt;p&gt;The Buyer will then submit a real Ethereum Sepolia transaction using test ETH.&lt;/p&gt;
&lt;p&gt;Finally, the backend should independently detect and verify the transaction before changing the order to:&lt;/p&gt;
&lt;p&gt;PAID&lt;/p&gt;
&lt;p&gt;and eventually:&lt;/p&gt;
&lt;p&gt;COMPLETED&lt;/p&gt;
&lt;p&gt;After the happy path works, we still need negative tests for wrong wallets, wrong networks, wrong recipients, incorrect amounts, failed transactions, insufficient confirmations, and reused transaction hashes.&lt;/p&gt;
&lt;p&gt;Why I’m building it this way&lt;/p&gt;
&lt;p&gt;I don’t want “Connect Wallet” to become a magic button that implicitly means identity, authorization, payment, and settlement.&lt;/p&gt;
&lt;p&gt;Each operation should have its own proof.&lt;/p&gt;
&lt;p&gt;OAuth&lt;br&gt;
→ proves the MyZubster account&lt;/p&gt;
&lt;p&gt;Wallet signature&lt;br&gt;
→ proves control of the address&lt;/p&gt;
&lt;p&gt;Order signature&lt;br&gt;
→ proves approval of the request&lt;/p&gt;
&lt;p&gt;Blockchain transaction&lt;br&gt;
→ proves value movement&lt;/p&gt;
&lt;p&gt;Server verifier&lt;br&gt;
→ proves that the transaction matches the order&lt;/p&gt;
&lt;p&gt;That makes the system more complex than a simple wallet-connect demo, but it also makes the trust model much clearer.&lt;/p&gt;
&lt;p&gt;The first major milestone is now working:&lt;/p&gt;
&lt;p&gt;a real authenticated MyZubster user can connect MetaMask, sign an off-chain challenge, and have their EVM wallet independently verified and linked by the backend.&lt;/p&gt;
&lt;p&gt;Next stop: the first complete Buyer → Seller transaction on Ethereum Sepolia.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28576/building-a-real-metamask-ethereum-sepolia-payment-flow-for-myzubster</guid>
<pubDate>Fri, 25 Sep 2026 05:53:49 +0000</pubDate>
</item>
</channel>
</rss>