Auditor: Darren Zal (Regen AI) Date: 2026-03-03 Repo: regen-compute v0.3.0 Scope: Full codebase review (~4,500 LOC + ~1,000 LOC tests)
Overall assessment: Solid foundation, well-structured, thoughtful design — but has several issues that need fixing before real money flows through.
What's good:
- Clean TypeScript, strict mode, ESM — no legacy JS patterns
- Two-phase payment provider abstraction (authorize → capture → refund) is well-designed
- Graceful degradation everywhere — every error falls back to marketplace links
- Database schema is well-normalized with proper FK constraints, WAL mode, transactions
- Revenue split math (85/5/10) handles rounding correctly — remainders go to ops/USS
- Good MCP server patterns — conditional tool registration, proper annotations, server instructions
- Comprehensive subscription flow (Stripe webhooks, referrals, magic links, customer portal)
What needs work:
- 3 security issues that could cause real problems in production
- Floating-point arithmetic used for financial calculations throughout
- No idempotency protection on on-chain transactions
- Dependency vulnerabilities (cosmjs/elliptic) with known advisories
- No rate limiting despite config field for it
File: src/server/routes.ts:570-573
Severity: Critical
When STRIPE_WEBHOOK_SECRET is not configured, the webhook endpoint accepts any POST body as a valid Stripe event, with no verification:
// In test mode without webhook secret, parse the raw body
const body = Buffer.isBuffer(req.body) ? req.body.toString("utf8") : req.body;
event = (typeof body === "string" ? JSON.parse(body) : body) as Stripe.Event;Impact: An attacker can POST fake checkout.session.completed events to credit arbitrary balances, create subscribers, or trigger referral rewards. This is fine for local dev but must not reach production without the webhook secret.
Fix: Either:
- Require
STRIPE_WEBHOOK_SECRETin production (fail startup without it), or - Only accept unverified webhooks when
NODE_ENV === "development"
File: src/config.ts:122 (config exists), no middleware applied
Severity: High
apiRateLimit: 100 is configured but never used. No rate limiting middleware exists on any route. The /debit, /checkout, /subscribe, and /webhook endpoints are all unprotected.
Impact: Financial endpoints can be hammered. The /subscribe endpoint creates Stripe Checkout Sessions, which have API costs. The /debit endpoint, while auth-protected, could be abused with a stolen API key.
Fix: Add express-rate-limit middleware, at minimum on /webhook, /checkout, /subscribe, /debit.
File: src/server/db.ts:717-727
Severity: Medium
verifyMagicLinkToken() performs SELECT then UPDATE as separate statements, not wrapped in a transaction:
const row = db.prepare("SELECT * FROM magic_links WHERE token = ? AND used = 0").get(token);
if (!row) return null;
if (new Date(row.expires_at) < new Date()) return null;
db.prepare("UPDATE magic_links SET used = 1 WHERE token = ?").run(token);Impact: Two concurrent requests with the same token could both pass the SELECT check. Use a single atomic UPDATE with a WHERE clause, or wrap in a transaction.
Fix:
const row = db.prepare(
"UPDATE magic_links SET used = 1 WHERE token = ? AND used = 0 AND expires_at > datetime('now') RETURNING email"
).get(token) as { email: string } | undefined;
return row?.email ?? null;npm audit: 9 vulnerabilities (4 low, 5 high)
| Package | Issue | Severity |
|---|---|---|
@cosmjs/crypto <=0.33.1 |
Uses elliptic with risky crypto implementation |
High |
axios (transitive) |
CSRF, SSRF, DoS vulnerabilities | High |
@confio/ics23 |
Unmaintained | Low |
The @cosmjs/crypto issue is the most concerning — it handles wallet key derivation. The @regen-network/api package pins old @cosmjs versions. Fix requires either:
- Upgrading
@regen-network/apito a version using@cosmjs>=0.34.0, or - Replacing the wallet code with direct
@cosmjs>=0.34.0 imports
Files: src/services/wallet.ts:16, src/services/evm-wallet.ts:39
Both wallet services cache mnemonics as module-level singletons (let _wallet). This is standard for CosmJS but means the mnemonic lives in plaintext in process memory for the entire server lifetime.
Mitigation: Acceptable for an MCP server (short-lived), but for the HTTP payment server (long-lived), consider using a KMS or at least zeroing the mnemonic after wallet init.
Multiple files use parseFloat() and floating-point arithmetic for amounts that represent real money:
| Location | Code | Issue |
|---|---|---|
order-selector.ts:124 |
parseFloat(order.quantity) |
Sell order quantities are strings; float conversion loses precision |
order-selector.ts:127 |
Math.min(remaining, available) |
Float subtraction accumulates errors across iterations |
order-selector.ts:131 |
BigInt(Math.ceil(take * 1_000_000)) |
Float multiply before BigInt — e.g., 0.1 * 1000000 = 100000.00000000001 |
retirement.ts:169 |
parseFloat(selection.totalQuantity) |
String → float for display |
pool.ts:388 |
sub.amount_cents / totalRevenueCents |
Integer division gives float fraction |
pool.ts:324 |
parseFloat(finalSelection.totalQuantity) |
Same pattern |
evm-wallet.ts:110 |
Math.round(amountUsdc * 10 ** decimals) |
Float multiply for token amounts |
Impact: Over many orders, the greedy fill loop in order-selector accumulates rounding errors. For small quantities, users might overpay or underpay by fractions of a micro-unit. For pool runs with many subscribers, attribution fractions may not sum to exactly 1.0.
Fix: Use a decimal library (e.g., decimal.js) or keep all amounts as BigInt micro-units throughout. The totalCostMicro field already uses BigInt correctly — extend this pattern to quantities.
File: src/services/retirement.ts
executeRetirement() has no mechanism to prevent double-execution. If called twice in quick succession:
- Both calls pass
checkPrepaidBalance() - Both calls pass
provider.authorizePayment()(crypto provider just checks balance) - Both calls
signAndBroadcast()— two transactions hit the chain
Impact: Double-spend of user's wallet funds. The prepaid balance would be debited twice as well.
Fix: Use a nonce/mutex pattern — either:
- A local lock (mutex) around the retirement flow
- An idempotency key passed by the caller, checked against recent tx hashes
- At minimum, re-check balance after acquiring a lock
File: src/services/retirement.ts:89-91
return `${config.marketplaceUrl}/projects/1?buying_options_filters=credit_card`;Always links to project ID "1" regardless of the credit class being retired. If the user asked to retire biodiversity credits (BT01), they'd get a link to project 1 (likely a carbon project).
Fix: Either make the project ID dynamic based on creditClass, or link to the marketplace search page with a credit type filter.
File: src/services/pool.ts:241
`--- REGEN Burn (10%) ---`The burn allocation is actually 5% of revenue (REVENUE_SPLIT.burn = 0.05). The 10% is operations. This is a display-only bug but could confuse auditors.
File: src/services/wallet.ts:50
gasPrice: GasPrice.fromString("0.025uregen"),Gas is set to auto for estimation multiplier but the base gas price is hardcoded. During network congestion or governance-changed gas minimums, transactions could fail.
Fix: Make configurable via REGEN_GAS_PRICE env var, with 0.025uregen as default.
All logging uses console.log/console.error with string interpolation. No log levels, no structured JSON, no request correlation IDs.
For a production payment server, structured logging (e.g., pino or winston) would help with debugging, monitoring, and audit trails.
Several catch blocks silently discard errors:
| Location | Pattern |
|---|---|
retirement.ts:253 |
try { await provider.refundPayment(auth.id); } catch { /* ignore */ } |
retirement.ts:50 |
catch { return null; } |
wallet.ts:41 |
regenProtoRegistry as ReadonlyArray<[string, any]> |
While graceful degradation is the right pattern for an MCP tool, the payment server should log these. A failed refund is something you want to know about.
src/server/routes.ts contains ~500 lines of inline HTML template strings. This is functional but makes the code hard to maintain and increases XSS surface area. The HTML rendering works but mixing presentation with business logic creates a large file.
Consider: Extract to a simple template engine, or use a static HTML file served separately.
The 3 test files cover ecobridge, pool, and evm-wallet — all with mocked dependencies. No tests exist for:
retirement.ts(core business logic)order-selector.ts(financial routing)routes.ts(HTTP API)wallet.ts(signing)
The most critical untested code is order-selector.ts — the greedy fill algorithm with floating-point math.
File: src/services/payment/stripe-stub.ts (45 LOC)
Three methods return "not implemented":
authorizePayment()→ should create a Stripe PaymentIntent withcapture_method: "manual"capturePayment()→ should callstripe.paymentIntents.capture()refundPayment()→ should callstripe.paymentIntents.cancel()orstripe.refunds.create()
The PaymentProvider interface is well-designed for this. The gap is wiring it to the Stripe SDK.
The CLI npx regen-compute pool-run works, but there's no automated trigger. Options:
- External cron job (simplest):
0 0 1 * * npx regen-compute pool-run - Internal scheduler (node-cron)
- Cloud scheduler (AWS EventBridge, GCP Cloud Scheduler)
The dashboard routes use magic link auth, but the actual dashboard implementation isn't in the reviewed code. The magic link flow exists in db.ts but the dashboard route handler would need to verify sessions.
- Add
NODE_ENVcheck — reject unverified webhooks in production - Add startup validation: warn or fail if
STRIPE_WEBHOOK_SECRETnot set in production - ~20 lines changed in
routes.ts
- Replace
parseFloat()with string-based decimal math inorder-selector.ts - Use BigInt consistently for all quantity calculations
- Add unit tests for edge cases (exact amounts, tiny fractions, multi-order fill)
- ~100 lines changed across
order-selector.ts,pool.ts
npm install express-rate-limit- Apply rate limiter to
/webhook,/checkout,/subscribe,/debit - Use the existing
apiRateLimitconfig value - ~30 lines in
server/index.tsorroutes.ts
- Replace SELECT+UPDATE with atomic UPDATE in
verifyMagicLinkToken - ~10 lines in
db.ts
- Fix burn percentage label (5% not 10%)
- Make gas price configurable
- Fix hardcoded marketplace project link
- ~15 lines across 3 files
Your codebase is well-structured and shows good engineering judgment. The MCP server design, payment provider abstraction, and graceful degradation patterns are solid. The architecture will scale to Phase 2 and 3 cleanly.
The items that need attention before real money flows:
- Set
STRIPE_WEBHOOK_SECRETin production — without it, anyone can fake payment events - Add rate limiting — your config has the field, just needs middleware
- Be careful with floating-point math in order-selector — consider using a decimal library for financial calculations
- Upgrade
@cosmjsdependencies when@regen-network/apireleases a version with@cosmjs>=0.34.0
Everything else is polish. The Stripe stub, pool scheduler, and dashboard are known Phase 2 items.