Skip to content

Add MPP to API submodule#495

Open
temitopeohassan wants to merge 1 commit intorecoupable:mainfrom
temitopeohassan:feat/mpp
Open

Add MPP to API submodule#495
temitopeohassan wants to merge 1 commit intorecoupable:mainfrom
temitopeohassan:feat/mpp

Conversation

@temitopeohassan
Copy link
Copy Markdown

@temitopeohassan temitopeohassan commented Apr 30, 2026

Implemented the Machine Payments Protocol (MPP) middleware in the api submodule.

Key Changes:

MPP Module: Created api/lib/mpp/ containing withMPP that wraps standard Next.js route handlers. This utility provides a unified hook for routes

Updated the following high-value endpoints to use the withMPP handler pattern:

Image Generation:
app/api/x402/image/generate/route.ts (Coinbase/x402 direct payment)
app/api/image/generate/route.ts (Public wrapper)
AI & Chat:
app/api/chat/route.ts (Streaming LLM responses)
Content Automation:
app/api/content/create/route.ts (Pipeline triggering)
Dependencies: Added stripe to api/package.json.

Environment Requirements:

STRIPE_SECRET_KEY: This environment variable is now required for Stripe payment verification to function correctly.

Summary by CodeRabbit

  • New Features
    • API endpoints now require payment authentication via Stripe integration.
    • Payment sessions support configurable budget limits with spend tracking.
    • Per-route pricing structure implemented across API operations.
    • Replay protection mechanism for payment transactions.
    • Session-based and payment-based authentication flows for API access.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 30, 2026

@temitopeohassan is attempting to deploy a commit to the Recoupable Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

📝 Walkthrough

Walkthrough

The pull request introduces a Micropayment Protocol (MPP) system that wraps API route handlers with middleware requiring payment verification. New modules handle session management, payment validation via Stripe, replay attack detection, route-based pricing, and the central withMPP middleware orchestrating the payment flow.

Changes

Cohort / File(s) Summary
API Route Wrapping
app/api/chat/route.ts, app/api/content/create/route.ts, app/api/image/generate/route.ts, app/api/x402/image/generate/route.ts
Each route's handler moved into internal handler function and exported via withMPP(handler), applying payment middleware to POST/GET endpoints.
Session Management
lib/mpp/session.ts
New in-memory session store with UUID-based sessions supporting budget tracking, spending limits, and 10-minute expiration windows. Exports createSession, getSession, and chargeSession functions.
Payment Verification
lib/mpp/providers/stripe.ts, lib/mpp/providers/index.ts
Introduces Stripe integration with verifyStripePayment for PaymentIntent verification; index exports unified verifyPayment function.
MPP Core Logic
lib/mpp/withMPP.ts, lib/mpp/pricing.ts, lib/mpp/replay.ts
Implements core middleware (withMPP) enforcing payment flow; route-based pricing (getPriceForRoute); and replay attack detection (isReplay) using in-memory ID registry.

Sequence Diagram

sequenceDiagram
    participant Client
    participant withMPP
    participant Session
    participant Payment
    participant Stripe
    participant Handler

    Client->>withMPP: Request with headers
    withMPP->>withMPP: Extract price from route

    alt Has x-mpp-session header
        withMPP->>Session: getSession(id)
        alt Session exists
            Session->>withMPP: Return session
            withMPP->>Session: chargeSession(id, price)
            alt Sufficient balance
                Session->>withMPP: true
                withMPP->>Handler: Delegate request
                Handler->>Client: Response (200)
            else Insufficient balance
                Session->>withMPP: false
                withMPP->>Client: insufficient_balance (402)
            end
        else Session missing
            Session->>withMPP: null
            withMPP->>Client: invalid_session (402)
        end
    else Has x-mpp-payment header
        withMPP->>withMPP: Check if replayed
        alt Is replay
            withMPP->>Client: replayed_payment (402)
        else New payment
            withMPP->>Payment: verifyPayment(paymentId)
            Payment->>Stripe: Retrieve PaymentIntent
            Stripe->>Payment: PaymentIntent
            Payment->>withMPP: boolean (succeeded?)
            alt Valid payment
                withMPP->>Handler: Delegate request
                Handler->>Client: Response (200)
            else Invalid payment
                withMPP->>Client: invalid_payment (402)
            end
        end
    else No payment headers
        withMPP->>Session: createSession(price)
        Session->>withMPP: Session created
        withMPP->>Client: payment_required (402) + session id
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • sweetmantech

Poem

💳 Micropayments flow through routes so bright,
Sessions charge smoothly, Stripe checks all's right,
Replays stay blocked in memory's keep,
While budgets and pricing their vigil do sweep!
🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Pull request contains multiple SOLID & Clean Code violations including file-naming mismatches, SRP breaches in session.ts, missing error handling, and unsuitable in-memory state management for distributed deployments. Refactor files to match exported functions (createSession.ts, getSession.ts, chargeSession.ts, getPriceForRoute.ts); add error handling to verifyPayment calls; migrate session/replay state to Redis with TTL; add CORS headers to error responses; require proof of payment before session issuance.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 10 files

Confidence score: 4/5

  • This PR looks safe to merge overall; the noted issues are maintainability/style concerns rather than clear runtime regressions.
  • lib/mpp/withMPP.ts uses Function and any types, which weakens type safety and could hide mistakes over time.
  • Module export patterns don’t align with filename conventions in lib/mpp/session.ts and lib/mpp/pricing.ts, which can reduce clarity and consistency but isn’t a functional break.
  • Pay close attention to lib/mpp/withMPP.ts, lib/mpp/session.ts, lib/mpp/pricing.ts - type looseness and export-name mismatches.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/mpp/withMPP.ts">

<violation number="1" location="lib/mpp/withMPP.ts:6">
P2: Custom agent: **Enforce Clear Code Style and Maintainability Practices**

Uses `Function` and `any` types instead of strict TypeScript typing</violation>
</file>

<file name="lib/mpp/session.ts">

<violation number="1" location="lib/mpp/session.ts:12">
P2: Custom agent: **Module should export a single primary function whose name matches the filename**

Module exports multiple functions instead of a single primary export matching the filename</violation>
</file>

<file name="lib/mpp/pricing.ts">

<violation number="1" location="lib/mpp/pricing.ts:1">
P2: Custom agent: **Module should export a single primary function whose name matches the filename**

Exported function name does not match filename basename</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant MPP as NEW: withMPP Middleware
    participant Session as NEW: Session Store (Memory)
    participant Stripe as NEW: Stripe API
    participant Handler as Original Route Handler

    Note over Client,Handler: Machine Payments Protocol (MPP) Flow

    Client->>MPP: GET/POST /api/[route]
    MPP->>MPP: NEW: getPriceForRoute(path)

    alt Session Authentication (x-mpp-session)
        MPP->>Session: getSession(sessionId)
        Session-->>MPP: session object (budget/spent)
        alt Session Valid & Sufficient Budget
            MPP->>Session: NEW: chargeSession(amount)
            MPP->>Handler: Call inner handler
            Handler-->>Client: 200 OK Response
        else Invalid/Expired/Insufficient
            MPP-->>Client: 402 Payment Required (error: status)
        end

    else Direct Payment (x-mpp-payment)
        MPP->>MPP: NEW: isReplay(paymentId)
        alt Not a Replay
            MPP->>Stripe: NEW: stripe.paymentIntents.retrieve()
            Note over Stripe: Uses STRIPE_SECRET_KEY
            Stripe-->>MPP: Payment Status
            alt Payment Succeeded
                MPP->>Handler: Call inner handler
                Handler-->>Client: 200 OK Response
            else Payment Failed
                MPP-->>Client: 402 Payment Required (error: invalid_payment)
            end
        else Replay Detected
            MPP-->>Client: 402 Payment Required (error: replayed_payment)
        end

    else No Payment Headers Provided
        MPP->>Session: NEW: createSession(default_budget)
        Session-->>MPP: new session object
        MPP-->>Client: 402 Payment Required (includes price + session)
    end
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread lib/mpp/withMPP.ts
import { createSession, getSession, chargeSession } from "./session"
import { isReplay } from "./replay"

export function withMPP(handler: Function) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Enforce Clear Code Style and Maintainability Practices

Uses Function and any types instead of strict TypeScript typing

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/mpp/withMPP.ts, line 6:

<comment>Uses `Function` and `any` types instead of strict TypeScript typing</comment>

<file context>
@@ -0,0 +1,56 @@
+import { createSession, getSession, chargeSession } from "./session"
+import { isReplay } from "./replay"
+
+export function withMPP(handler: Function) {
+  return async (req: Request, context?: any) => {
+    const path = new URL(req.url).pathname
</file context>

Comment thread lib/mpp/session.ts
@@ -0,0 +1,44 @@
import crypto from "crypto"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Module should export a single primary function whose name matches the filename

Module exports multiple functions instead of a single primary export matching the filename

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/mpp/session.ts, line 12:

<comment>Module exports multiple functions instead of a single primary export matching the filename</comment>

<file context>
@@ -0,0 +1,44 @@
+
+const sessions = new Map<string, Session>()
+
+export function createSession(budget = 1.0): Session {
+  const session = {
+    id: crypto.randomUUID(),
</file context>

Comment thread lib/mpp/pricing.ts
@@ -0,0 +1,7 @@
export function getPriceForRoute(path: string): number {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Module should export a single primary function whose name matches the filename

Exported function name does not match filename basename

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/mpp/pricing.ts, line 1:

<comment>Exported function name does not match filename basename</comment>

<file context>
@@ -0,0 +1,7 @@
+export function getPriceForRoute(path: string): number {
+  if (path.includes("/ai")) return 0.002
+  if (path.includes("/data")) return 0.001
</file context>

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/api/image/generate/route.ts (1)

33-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not accept account_id from request input for this route.

Passing through account_id from query violates the auth model and allows cross-account spoofing. Derive account context via validateAuthContext() and remove account_id from request parsing/contracts.

As per coding guidelines, “Never use account_id in request bodies or tool schemas; always derive the account ID from authentication” and “Always use validateAuthContext() for authentication in API routes.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/image/generate/route.ts` around lines 33 - 39, The route currently
accepts account_id from validatedQuery which allows spoofing; remove account_id
from request parsing/contracts and instead call validateAuthContext() in this
handler to derive the authenticated account ID, then pass that derived ID to
x402GenerateImage (replace usage of validatedQuery.account_id). Update any
request validation/schema to drop account_id and ensure x402GenerateImage
invocation uses the account id returned from validateAuthContext() (and not
request input).
🧹 Nitpick comments (2)
app/api/x402/image/generate/route.ts (1)

29-59: ⚡ Quick win

Use a dedicated Zod validate function for request parsing.

This route currently does ad-hoc query parsing/validation. Please move prompt/files validation into a route-specific Zod validator (same pattern used elsewhere) so success/error paths stay consistent and easier to maintain.

As per coding guidelines, “All API endpoints should use a validate function for input parsing using Zod for schema validation.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/x402/image/generate/route.ts` around lines 29 - 59, The route does
ad-hoc parsing for prompt and files; introduce a route-specific Zod validator
(e.g., create a validateGenerateImageRequest(schema) function or
validateGenerateImageRequest using z.object({ prompt: z.string().min(1), files:
z.string().optional() })) and replace the inline checks with a single call to
that validate function to parse request.url searchParams; call
parseFilesFromQuery inside the validator or as a transform in the Zod schema so
the route receives { prompt, files, account } already validated; on validation
failure return the same NextResponse.json shape (status 400, getCorsHeaders())
using the validator's error message(s) to populate the details field; update
references in route.ts including parseFilesFromQuery and getBuyerAccount to use
the validated output.
lib/mpp/withMPP.ts (1)

6-56: 🏗️ Heavy lift

Refactor withMPP into smaller helpers.

This function currently combines pricing, header parsing, session flow, direct-payment flow, and response shaping in one block. Extracting flow-specific helpers will improve readability and testability.

As per coding guidelines, "Flag functions longer than 20 lines" and "Keep functions small and focused".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/withMPP.ts` around lines 6 - 56, withMPP is doing routing, pricing,
header parsing, session flow, payment verification, and response shaping in one
large function; split responsibilities by extracting small helpers: create
handleSessionFlow(req, context, sessionId, price) to encapsulate getSession,
chargeSession and return either error Responses or call handler, create
handlePaymentFlow(req, context, payment) to encapsulate isReplay, verifyPayment
and return error Responses or call handler, and create
buildPaymentRequiredResponse(price) (or createSessionAndResponse) to wrap
createSession plus the 402 response; keep existing calls to getPriceForRoute,
getSession, chargeSession, isReplay, verifyPayment, createSession and ensure
withMPP simply parses headers, calls these helpers and returns their result so
behavior remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/chat/route.ts`:
- Line 44: withMPP middleware is returning 402/payment-required or other error
Responses without CORS headers which breaks browser clients; update withMPP so
that any early Response it returns (e.g., the 402 Response or other error
Responses created inside withMPP) includes the same CORS headers used by
successful responses (reuse whatever CORS header object your app uses or
construct the standard Access-Control-Allow-* headers) so wrapped routes like
POST = withMPP(handler) always send CORS headers even on errors; locate the
withMPP function and ensure every branch that returns new Response(...)
merges/injects the CORS headers before returning.

In `@lib/mpp/pricing.ts`:
- Around line 1-7: The exported function getPriceForRoute must live in a file
named after it: create a new file getPriceForRoute.ts containing the existing
named export function getPriceForRoute(path: string): number (copy the logic
unchanged), remove the function from pricing.ts, and update any imports to
import { getPriceForRoute } from './getPriceForRoute' (or add a re-export in
your barrel/index if you prefer to keep the old import surface).

In `@lib/mpp/providers/index.ts`:
- Around line 3-5: Move the exported function verifyPayment out of index.ts into
a new file named verifyPayment.ts and implement it there to call
verifyStripePayment (so verifyPayment(payment: string) { return
verifyStripePayment(payment) }); then update index.ts to be a pure barrel that
re-exports verifyPayment (e.g., export { verifyPayment } from
'./verifyPayment'); ensure the exported symbol names remain verifyPayment and
verifyStripePayment so imports elsewhere continue to work.

In `@lib/mpp/replay.ts`:
- Around line 1-6: The replay registry currently uses an ever-growing Set named
used and function isReplay, which will leak memory; replace it with a
bounded/TTL cache (e.g., Map<string, number> storing timestamps or an LRU cache)
and evict old entries: change used from Set to a Map or an LRU instance, update
isReplay to check timestamp/expiry and treat expired entries as not used
(removing them), insert/refresh the id with the current time when seen, and add
a periodic cleanup (setInterval) or configure maxSize/TTL via an LRU library to
ensure entries are pruned and the registry cannot grow unbounded.
- Around line 3-6: The isReplay function currently both checks and records
(mutating the shared used set) — split it into a read-only hasReplay(id) that
returns used.has(id) without modifying state, and a separate markReplay(id) that
performs used.add(id); update any callers (e.g., where replay checking occurs
before payment verification) to call hasReplay(id) to decide if it’s a replay
and only call markReplay(id) after the payment/verification step succeeds; keep
the existing used Set and function names (isReplay -> hasReplay + markReplay) so
locating the change is straightforward.

In `@lib/mpp/session.ts`:
- Around line 12-44: This file exports multiple session functions violating
SRP/file-naming rules; split each exported function into its own file named
after the function (createSession.ts, getSession.ts, chargeSession.ts) and
update imports/exports accordingly: move the shared in-memory store and types
(sessions Map, Session type, any crypto usage and TTL constant) into a small
shared module (e.g., sessionStore.ts) that exports the sessions Map and Session
type, then have createSession, getSession and chargeSession import that store
and implement their logic unchanged; ensure all call sites are updated to import
the new individual functions from their new files.
- Around line 10-34: The in-memory Map named sessions causes lost state across
instances/restarts; replace it with a shared Redis-backed store: update
createSession to generate the session object (id, budget, spent, expiresAt) and
persist it to Redis (SET key=session:{id} value=JSON(session) with an
appropriate EX TTL), update getSession to GET and JSON.parse the stored session,
treat missing/expired keys as null (no manual expiry checks), and use DEL to
delete sessions; remove or stop using the local sessions Map and ensure updates
to spent/budget use Redis commands (or a small Lua script/transaction) to avoid
race conditions when modifying session state.

In `@lib/mpp/withMPP.ts`:
- Around line 32-34: The replay check in withMPP uses isReplay which relies on
the process-local in-memory Set in lib/mpp/replay.ts and is unsafe across
replicas; replace that local store with a distributed atomic claim using Redis
(or similar) so replay keys are unique cluster-wide. Update lib/mpp/replay.ts to
expose an atomic claim function (e.g., claimReplayKey / markReplay) that uses
SET key value NX EX <ttl> (or SETNX + EXPIRE) and returns whether the key was
newly set; then change withMPP to call that claim function instead of isReplay
(and treat a failed claim as a replay) so the Response.json({ error:
"replayed_payment" }, { status: 402 }) branch is enforced across instances.
Ensure the TTL is configurable and errors from the store fall back to
failing-safe (treat as replay or log and reject) to avoid weakening protection.
- Around line 45-53: The code currently calls createSession() before payment
proof verification and returns that spendable session in the 402 response,
allowing clients to repeatedly obtain usable sessions for free; move the
createSession() call so it only executes after payment has been validated (e.g.,
after your payment proof verification routine), and change the 402 Response.json
payload to omit any spendable session or return a non-spendable placeholder; in
short, ensure createSession() is invoked only on successful payment verification
and Response.json (error: "payment_required") does not include the session.
- Around line 36-40: In withMPP middleware, verifyPayment can throw and
currently bubbles an unhandled error; wrap the call to verifyPayment in a
try/catch inside the withMPP function, log the provider error, and return a
controlled Response.json error (e.g. { error: "payment_provider_error" } with an
appropriate 5xx status such as 502) instead of letting the exception propagate;
keep the existing invalid_payment response for false returns from verifyPayment.

---

Outside diff comments:
In `@app/api/image/generate/route.ts`:
- Around line 33-39: The route currently accepts account_id from validatedQuery
which allows spoofing; remove account_id from request parsing/contracts and
instead call validateAuthContext() in this handler to derive the authenticated
account ID, then pass that derived ID to x402GenerateImage (replace usage of
validatedQuery.account_id). Update any request validation/schema to drop
account_id and ensure x402GenerateImage invocation uses the account id returned
from validateAuthContext() (and not request input).

---

Nitpick comments:
In `@app/api/x402/image/generate/route.ts`:
- Around line 29-59: The route does ad-hoc parsing for prompt and files;
introduce a route-specific Zod validator (e.g., create a
validateGenerateImageRequest(schema) function or validateGenerateImageRequest
using z.object({ prompt: z.string().min(1), files: z.string().optional() })) and
replace the inline checks with a single call to that validate function to parse
request.url searchParams; call parseFilesFromQuery inside the validator or as a
transform in the Zod schema so the route receives { prompt, files, account }
already validated; on validation failure return the same NextResponse.json shape
(status 400, getCorsHeaders()) using the validator's error message(s) to
populate the details field; update references in route.ts including
parseFilesFromQuery and getBuyerAccount to use the validated output.

In `@lib/mpp/withMPP.ts`:
- Around line 6-56: withMPP is doing routing, pricing, header parsing, session
flow, payment verification, and response shaping in one large function; split
responsibilities by extracting small helpers: create handleSessionFlow(req,
context, sessionId, price) to encapsulate getSession, chargeSession and return
either error Responses or call handler, create handlePaymentFlow(req, context,
payment) to encapsulate isReplay, verifyPayment and return error Responses or
call handler, and create buildPaymentRequiredResponse(price) (or
createSessionAndResponse) to wrap createSession plus the 402 response; keep
existing calls to getPriceForRoute, getSession, chargeSession, isReplay,
verifyPayment, createSession and ensure withMPP simply parses headers, calls
these helpers and returns their result so behavior remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0076a79e-b6fb-4fca-a459-64eff5ab2dee

📥 Commits

Reviewing files that changed from the base of the PR and between 5487356 and 3ef8cbf.

📒 Files selected for processing (10)
  • app/api/chat/route.ts
  • app/api/content/create/route.ts
  • app/api/image/generate/route.ts
  • app/api/x402/image/generate/route.ts
  • lib/mpp/pricing.ts
  • lib/mpp/providers/index.ts
  • lib/mpp/providers/stripe.ts
  • lib/mpp/replay.ts
  • lib/mpp/session.ts
  • lib/mpp/withMPP.ts

Comment thread app/api/chat/route.ts
return handleChatStream(request);
}

export const POST = withMPP(handler);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ensure withMPP returns CORS headers on 402/error responses.

After wrapping POST, middleware-generated payment_required/payment errors become part of this route contract. Those responses currently miss CORS headers, which can block browser clients. Please add CORS headers in withMPP error responses (this impacts all wrapped routes).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/chat/route.ts` at line 44, withMPP middleware is returning
402/payment-required or other error Responses without CORS headers which breaks
browser clients; update withMPP so that any early Response it returns (e.g., the
402 Response or other error Responses created inside withMPP) includes the same
CORS headers used by successful responses (reuse whatever CORS header object
your app uses or construct the standard Access-Control-Allow-* headers) so
wrapped routes like POST = withMPP(handler) always send CORS headers even on
errors; locate the withMPP function and ensure every branch that returns new
Response(...) merges/injects the CORS headers before returning.

Comment thread lib/mpp/pricing.ts
Comment on lines +1 to +7
export function getPriceForRoute(path: string): number {
if (path.includes("/ai")) return 0.002
if (path.includes("/data")) return 0.001
if (path.includes("/trade")) return 0.01

return 0.0005
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Move this export to a filename that matches the function name.

pricing.ts exports getPriceForRoute, which violates the repo rule requiring lib/**/*.ts files to be named after their exported function. Please move this to getPriceForRoute.ts (and optionally re-export from an index/barrel if needed).

As per coding guidelines, “File naming rule: The file name MUST match the exported function name.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/pricing.ts` around lines 1 - 7, The exported function
getPriceForRoute must live in a file named after it: create a new file
getPriceForRoute.ts containing the existing named export function
getPriceForRoute(path: string): number (copy the logic unchanged), remove the
function from pricing.ts, and update any imports to import { getPriceForRoute }
from './getPriceForRoute' (or add a re-export in your barrel/index if you prefer
to keep the old import surface).

Comment on lines +3 to +5
export async function verifyPayment(payment: string) {
return verifyStripePayment(payment)
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use a function-matching filename for this export.

index.ts directly exporting verifyPayment conflicts with the lib/**/*.ts file naming rule. Move the function to verifyPayment.ts and keep index.ts as a pure re-export barrel if needed.

As per coding guidelines, “The file name MUST match the exported function name.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/providers/index.ts` around lines 3 - 5, Move the exported function
verifyPayment out of index.ts into a new file named verifyPayment.ts and
implement it there to call verifyStripePayment (so verifyPayment(payment:
string) { return verifyStripePayment(payment) }); then update index.ts to be a
pure barrel that re-exports verifyPayment (e.g., export { verifyPayment } from
'./verifyPayment'); ensure the exported symbol names remain verifyPayment and
verifyStripePayment so imports elsewhere continue to work.

Comment thread lib/mpp/replay.ts
Comment on lines +1 to +6
const used = new Set<string>()

export function isReplay(id: string) {
if (used.has(id)) return true
used.add(id)
return false
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replay registry needs TTL/eviction to avoid unbounded growth.

used grows forever for process lifetime. Add expiry/pruning (or move to bounded shared storage) so replay tracking doesn’t become a memory leak under sustained traffic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/replay.ts` around lines 1 - 6, The replay registry currently uses an
ever-growing Set named used and function isReplay, which will leak memory;
replace it with a bounded/TTL cache (e.g., Map<string, number> storing
timestamps or an LRU cache) and evict old entries: change used from Set to a Map
or an LRU instance, update isReplay to check timestamp/expiry and treat expired
entries as not used (removing them), insert/refresh the id with the current time
when seen, and add a periodic cleanup (setInterval) or configure maxSize/TTL via
an LRU library to ensure entries are pruned and the registry cannot grow
unbounded.

Comment thread lib/mpp/replay.ts
Comment on lines +3 to +6
export function isReplay(id: string) {
if (used.has(id)) return true
used.add(id)
return false
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

isReplay should not mutate state during the “check” step.

This API both checks and records, which is fragile with the current middleware flow (replay check happens before payment verification). A transient verification failure can permanently poison a payment ID as replay. Split into hasReplay(id) and markReplay(id) and only mark after successful verification.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/replay.ts` around lines 3 - 6, The isReplay function currently both
checks and records (mutating the shared used set) — split it into a read-only
hasReplay(id) that returns used.has(id) without modifying state, and a separate
markReplay(id) that performs used.add(id); update any callers (e.g., where
replay checking occurs before payment verification) to call hasReplay(id) to
decide if it’s a replay and only call markReplay(id) after the
payment/verification step succeeds; keep the existing used Set and function
names (isReplay -> hasReplay + markReplay) so locating the change is
straightforward.

Comment thread lib/mpp/session.ts
Comment on lines +10 to +34
const sessions = new Map<string, Session>()

export function createSession(budget = 1.0): Session {
const session = {
id: crypto.randomUUID(),
budget,
spent: 0,
expiresAt: Date.now() + 10 * 60 * 1000
}

sessions.set(session.id, session)
return session
}

export function getSession(id: string): Session | null {
const s = sessions.get(id)
if (!s) return null

if (Date.now() > s.expiresAt) {
sessions.delete(id)
return null
}

return s
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

In-memory session state will break across instances/restarts.

Using a process-local Map for payment sessions can return false invalid_session / balance failures in serverless or multi-replica deployments. This state should be moved to shared storage (e.g., Redis) with TTL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/session.ts` around lines 10 - 34, The in-memory Map named sessions
causes lost state across instances/restarts; replace it with a shared
Redis-backed store: update createSession to generate the session object (id,
budget, spent, expiresAt) and persist it to Redis (SET key=session:{id}
value=JSON(session) with an appropriate EX TTL), update getSession to GET and
JSON.parse the stored session, treat missing/expired keys as null (no manual
expiry checks), and use DEL to delete sessions; remove or stop using the local
sessions Map and ensure updates to spent/budget use Redis commands (or a small
Lua script/transaction) to avoid race conditions when modifying session state.

Comment thread lib/mpp/session.ts
Comment on lines +12 to +44
export function createSession(budget = 1.0): Session {
const session = {
id: crypto.randomUUID(),
budget,
spent: 0,
expiresAt: Date.now() + 10 * 60 * 1000
}

sessions.set(session.id, session)
return session
}

export function getSession(id: string): Session | null {
const s = sessions.get(id)
if (!s) return null

if (Date.now() > s.expiresAt) {
sessions.delete(id)
return null
}

return s
}

export function chargeSession(id: string, amount: number): boolean {
const s = getSession(id)
if (!s) return false

if (s.spent + amount > s.budget) return false

s.spent += amount
return true
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Split session operations into function-named files under lib/.

This file exports multiple domain functions from one lib/**/*.ts module, which conflicts with the repo’s SRP/file-naming constraints.

As per coding guidelines, “Apply Single Responsibility Principle (SRP): one exported function per file; each file should do one thing well” and “The file name MUST match the exported function name.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/session.ts` around lines 12 - 44, This file exports multiple session
functions violating SRP/file-naming rules; split each exported function into its
own file named after the function (createSession.ts, getSession.ts,
chargeSession.ts) and update imports/exports accordingly: move the shared
in-memory store and types (sessions Map, Session type, any crypto usage and TTL
constant) into a small shared module (e.g., sessionStore.ts) that exports the
sessions Map and Session type, then have createSession, getSession and
chargeSession import that store and implement their logic unchanged; ensure all
call sites are updated to import the new individual functions from their new
files.

Comment thread lib/mpp/withMPP.ts
Comment on lines +32 to +34
if (isReplay(payment)) {
return Response.json({ error: "replayed_payment" }, { status: 402 })
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: replay protection is not safe across instances.

This branch depends on isReplay(...), and the current replay store is process-local (lib/mpp/replay.ts in-memory Set). In multi-replica deployments, the same payment can be replayed against different instances.

Use a shared atomic store (e.g., Redis SETNX + TTL) for replay keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/withMPP.ts` around lines 32 - 34, The replay check in withMPP uses
isReplay which relies on the process-local in-memory Set in lib/mpp/replay.ts
and is unsafe across replicas; replace that local store with a distributed
atomic claim using Redis (or similar) so replay keys are unique cluster-wide.
Update lib/mpp/replay.ts to expose an atomic claim function (e.g.,
claimReplayKey / markReplay) that uses SET key value NX EX <ttl> (or SETNX +
EXPIRE) and returns whether the key was newly set; then change withMPP to call
that claim function instead of isReplay (and treat a failed claim as a replay)
so the Response.json({ error: "replayed_payment" }, { status: 402 }) branch is
enforced across instances. Ensure the TTL is configurable and errors from the
store fall back to failing-safe (treat as replay or log and reject) to avoid
weakening protection.

Comment thread lib/mpp/withMPP.ts
Comment on lines +36 to +40
const valid = await verifyPayment(payment)

if (!valid) {
return Response.json({ error: "invalid_payment" }, { status: 402 })
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle provider failures from verifyPayment explicitly.

If the provider call rejects/throws, the middleware will bubble an unhandled error. Return a controlled payment-provider error response instead.

Proposed fix
-      const valid = await verifyPayment(payment)
+      let valid = false
+      try {
+        valid = await verifyPayment(payment)
+      } catch {
+        return Response.json(
+          { error: "payment_verification_unavailable" },
+          { status: 502 }
+        )
+      }

As per coding guidelines, "Handle errors gracefully".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/withMPP.ts` around lines 36 - 40, In withMPP middleware,
verifyPayment can throw and currently bubbles an unhandled error; wrap the call
to verifyPayment in a try/catch inside the withMPP function, log the provider
error, and return a controlled Response.json error (e.g. { error:
"payment_provider_error" } with an appropriate 5xx status such as 502) instead
of letting the exception propagate; keep the existing invalid_payment response
for false returns from verifyPayment.

Comment thread lib/mpp/withMPP.ts
Comment on lines +45 to +53
const session = createSession()

return Response.json(
{
error: "payment_required",
price,
session
},
{ status: 402 }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: unfunded session issuance allows unlimited free usage.

Line 45 creates a fresh spendable session before any payment proof is verified. A client can repeatedly request new sessions and bypass paid access entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/withMPP.ts` around lines 45 - 53, The code currently calls
createSession() before payment proof verification and returns that spendable
session in the 402 response, allowing clients to repeatedly obtain usable
sessions for free; move the createSession() call so it only executes after
payment has been validated (e.g., after your payment proof verification
routine), and change the 402 Response.json payload to omit any spendable session
or return a non-spendable placeholder; in short, ensure createSession() is
invoked only on successful payment verification and Response.json (error:
"payment_required") does not include the session.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant