Add MPP to API submodule#495
Conversation
|
@temitopeohassan is attempting to deploy a commit to the Recoupable Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThe 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 Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
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.tsusesFunctionandanytypes, which weakens type safety and could hide mistakes over time.- Module export patterns don’t align with filename conventions in
lib/mpp/session.tsandlib/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
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| import { createSession, getSession, chargeSession } from "./session" | ||
| import { isReplay } from "./replay" | ||
|
|
||
| export function withMPP(handler: Function) { |
There was a problem hiding this comment.
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>
| @@ -0,0 +1,44 @@ | |||
| import crypto from "crypto" | |||
There was a problem hiding this comment.
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>
| @@ -0,0 +1,7 @@ | |||
| export function getPriceForRoute(path: string): number { | |||
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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 winDo not accept
account_idfrom request input for this route.Passing through
account_idfrom query violates the auth model and allows cross-account spoofing. Derive account context viavalidateAuthContext()and removeaccount_idfrom request parsing/contracts.As per coding guidelines, “Never use
account_idin request bodies or tool schemas; always derive the account ID from authentication” and “Always usevalidateAuthContext()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 winUse 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 liftRefactor
withMPPinto 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
📒 Files selected for processing (10)
app/api/chat/route.tsapp/api/content/create/route.tsapp/api/image/generate/route.tsapp/api/x402/image/generate/route.tslib/mpp/pricing.tslib/mpp/providers/index.tslib/mpp/providers/stripe.tslib/mpp/replay.tslib/mpp/session.tslib/mpp/withMPP.ts
| return handleChatStream(request); | ||
| } | ||
|
|
||
| export const POST = withMPP(handler); |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
🛠️ 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).
| export async function verifyPayment(payment: string) { | ||
| return verifyStripePayment(payment) | ||
| } No newline at end of file |
There was a problem hiding this comment.
🛠️ 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.
| const used = new Set<string>() | ||
|
|
||
| export function isReplay(id: string) { | ||
| if (used.has(id)) return true | ||
| used.add(id) | ||
| return false |
There was a problem hiding this comment.
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.
| export function isReplay(id: string) { | ||
| if (used.has(id)) return true | ||
| used.add(id) | ||
| return false |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
🛠️ 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.
| if (isReplay(payment)) { | ||
| return Response.json({ error: "replayed_payment" }, { status: 402 }) | ||
| } |
There was a problem hiding this comment.
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.
| const valid = await verifyPayment(payment) | ||
|
|
||
| if (!valid) { | ||
| return Response.json({ error: "invalid_payment" }, { status: 402 }) | ||
| } |
There was a problem hiding this comment.
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.
| const session = createSession() | ||
|
|
||
| return Response.json( | ||
| { | ||
| error: "payment_required", | ||
| price, | ||
| session | ||
| }, | ||
| { status: 402 } |
There was a problem hiding this comment.
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.
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