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.
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.
| Requirement | Node.js (V4) | Go (V5) |
|---|---|---|
| Concurrent connections | ~1K (single thread) | 10K+ (goroutines) |
| Memory after 24h idle | 143MB | ~15MB (estimated) |
| Startup time | 2-4s | <100ms |
| Deployment artifact | node_modules + 200 files | Single binary |
| Graceful shutdown | Manual | Built-in (context) |
| Type safety | Runtime crashes | Compile-time |
Three specific things pushed us to Go:
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()
}
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()
}
}
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.
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 });
}
};
We're running V4 and V5 side-by-side during migration:
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.
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