⚡ Agent Developers300+ pay-per-call APIs. Give your agent a wallet in 10 seconds.Get 500 Free Credits →

From Node.js to Go — Why We're Rewriting minia2a's Gateway

August 7, 2026 · 6 min read · goarchitecturex402engineering

The Problem

minia2a started as a Node.js monolith. One Express server handling everything: 323 API endpoints, x402 payment challenges, MCP JSON-RPC, trial economy, agent registry, and blog serving. It worked. Until it didn't.

In the last 72 hours, the V4 Node process restarted 49 times. Not from traffic spikes — from accumulated technical debt:

These aren't Node.js's fault. They're what happens when a prototype grows into infrastructure without a concurrency model designed for it.

What Changed

V5 splits the monolith into two processes:

Client → Go Gateway (:80) → Node Sidecar (:3000) → 199 Service Handlers
              ↑                       ↑
         trial check,             individual .js files
         x402 challenges,         (drop-in from V4,
         payment receipts,        no modification)
         API routing

Go Gateway (Gin + modernc/sqlite): Handles HTTP, x402 payment challenges, rate limiting, JWT auth, and trial economy. Uses pure-Go SQLite (no CGO) so deployment is a single binary.

Node Sidecar (Express): Runs the 199 individual service handler files. These are the actual API implementations — CAPTCHA solving, gas price lookups, Polymarket data — and they don't need to change. The Go gateway proxies to them over localhost.

Why Go for the Gateway

RequirementNode.js (V4)Go (V5)
Concurrent connections~1K (single thread)10K+ (goroutines)
Memory after 24h idle143MB~15MB (estimated)
Startup time2-4s<100ms
Deployment artifactnode_modules + 200 filesSingle binary
Graceful shutdownManualBuilt-in (context)
Type safetyRuntime crashesCompile-time

Three specific things pushed us to Go:

1. x402 Payment Idempotency Needs Atomicity

When an agent pays for an API call, the gateway must atomically: (a) verify the payment on-chain, (b) store the receipt, (c) forward the request, (d) return the response. In Node.js, this required careful async/await coordination with no compile-time guarantees. In Go, the control flow is explicit and the compiler catches missing error paths.

// V5: receipt verification is a single read-through path
func (g *Gate) VerifyOrChallenge(c *gin.Context, db *sql.DB) {
    sig := c.GetHeader("x-payment-sig")
    if sig == "" {
        g.SendChallenge(c)
        return
    }
    receipt, err := g.VerifyReceipt(sig)
    if err != nil {
        g.SendChallenge(c)
        return
    }
    // Receipt is valid — forward to service
    c.Set("receipt", receipt)
    c.Next()
}

2. Rate Limiting Per-Endpoint, Per-IP

V5's rate limiter is a token-bucket implementation with per-IP counters and configurable per-endpoint limits. POST /api/v1/register-simple gets 5 req/min. GET /x402/gas gets 30. The Go implementation is 60 lines with no external dependencies.

func rateLimit(maxPerMinute int) gin.HandlerFunc {
    return func(c *gin.Context) {
        ip := c.ClientIP()
        // token bucket: count requests per IP, reset every 60s
        b.count++
        if b.count > maxPerMinute {
            c.AbortWithStatusJSON(429, gin.H{"error": "rate limit exceeded"})
            return
        }
        c.Next()
    }
}

3. Single Binary Deployment

The Go gateway compiles to a single static binary. No npm install, no node_modules, no runtime version mismatches. Deploy with scp gateway user@host: and restart. This matters when you're pushing fixes at 2am.

What Stays in Node.js

The 199 service handlers stay in JavaScript. These are thin wrappers — most are 20-40 lines calling external APIs or doing simple transformations. Rewriting them in Go would be busy work with no performance gain (they're I/O bound). The sidecar approach means we migrate what matters (the gateway) and leave what works (the handlers).

// compat/services/x402-gas.js — stays exactly as-is
module.exports = async function(req, res) {
  const chain = req.query.chain || "ethereum";
  try {
    const gas = await fetchGasPrice(chain);
    res.json({ ok: true, gas: gas.fast, unit: "gwei", chain });
  } catch (e) {
    res.json({ ok: false, error: e.message });
  }
};

Migration Path

We're running V4 and V5 side-by-side during migration:

  1. Phase 1 (done): Go gateway builds, passes tests, serves all API routes
  2. Phase 2 (current): Parity testing — V5 handles a percentage of production traffic, comparing responses with V4
  3. Phase 3 (next): Full cutover — V5 on :80, V4 as fallback on :8081

Numbers That Matter

From V4 production (last 30 days, via /api/stats):

These aren't VC-pitch numbers. They're real infrastructure numbers from a live marketplace where the traffic is 99% agent-originated, not human browsing.

Key insight: The 49 V4 restarts weren't from load — they were from complexity. When your payment gateway crashes because a curl example string in a JSON response has an unescaped quote, the problem isn't the language. It's that payment-critical code and content strings lived in the same file. V5 separates them into gateway (Go) and content (Node sidecar).

What's Next

V5 is open source (MIT). If you're building agent-to-agent payment infrastructure and want to contribute — or just want to see how an x402 gateway works in Go — the code is at minia2a-v5/gateway/.

The gateway currently handles: health checks, stats API, service registry, x402 payment challenges, receipt verification, MCP JSON-RPC, rate limiting, agent registration, credit purchases, and trial economy. Each feature is ~100-200 lines of Go with explicit error handling.

If you're running a Node.js payment service and considering a Go rewrite, our advice: don't rewrite everything. Move the gateway (HTTP, auth, payments) to Go. Leave the business logic where it is. The sidecar pattern works.

Discuss on GitHub · Try the API: curl https://minia2a.uk/api/stats · Build with x402: minia2a.uk/build-x402-agent.html