Skip to content

feat(payment): add Chainrails cross-chain payment module#51

Open
GoodyBoy301 wants to merge 42 commits intokeep-starknet-strange:mainfrom
horuslabsio:payment
Open

feat(payment): add Chainrails cross-chain payment module#51
GoodyBoy301 wants to merge 42 commits intokeep-starknet-strange:mainfrom
horuslabsio:payment

Conversation

@GoodyBoy301
Copy link

@GoodyBoy301 GoodyBoy301 commented Mar 2, 2026

Summary

This PR introduces the Payment module powered by ChainRails, enabling cross-chain, multi-token payment acceptance for StarkZap applications.

Features Added

  • Payment Session Management: Create payment sessions for recipients with specific token and amount preferences
  • Quote Discovery: Get quotes from all available bridges or find the best quote across bridges
  • Payment Intents: Create, manage, and track payment intents with full lifecycle support
  • Cross-Chain Routing: Find optimal routes and supported bridges for any source → destination chain pair
  • Chain & Balance Queries: Query supported chains and token balances across chains
  • Payment Modal: Web-based payment modal using ChainRails vanilla web component
  • Merchant Info: Retrieve client/merchant account information

Technical Details

  • Integrated @chainrails/sdk (v0.3.3) and @chainrails/vanilla (v0.0.6)
  • Added payment config option to SDKConfig with apiKey and optional environment
  • Browser runtime detection for graceful fallback when payment is not configured
  • Full TypeScript support with comprehensive type definitions

Usage Example

Get Quotes and Create Intent

// Get all quotes for a destination
const quotes = await payment.getAllQuotes({
  destinationChain: "STARKNET",
  tokenOut: "0x053c91...",
  amount: "10",
  recipient: "0xabc...",
});

// Create a payment intent
const intent = await payment.createIntent({
  sender: "0xsender...",
  amount: "10",
  tokenIn: "0xtokenIn...",
  amountSymbol: "USDC",
  source_chain: "BASE",
  destination_chain: "STARKNET",
  recipient: "0xrecipient...",
  refund_address: "0xsender...",
});

Payment Modal (Web)

// Create a modal handle with the session token
const modalHandle = payment.modal({
  sessionToken: session.sessionToken,
  amount: "25.00",
  platform: "web", // defaults to "web"
});

// Trigger the payment modal - returns true on success, false on cancel
const success = await modalHandle.pay();

if (success) {
  console.log("Payment completed successfully!");
} else {
  console.log("Payment was cancelled or failed");
}

eg. A variant of flappy-bird where the rich live forever
https://github.com/user-attachments/assets/83ea0649-e7aa-4790-9d2a-50a8401ea525

Tests

Added comprehensive test suite with 24 test cases covering:

  • Payment constants (chains, tokens, bridges)
  • Session creation
  • Modal handling
  • Quote fetching (all, best, single bridge)
  • Intent management (create, get, list, trigger processing)
  • Router operations
  • Chain queries
  • Client info retrieval
  • SDK integration

Breaking Changes

None - Payment module is optional and only activated when payment config is provided.

Related Dependencies

@chainrails/sdk: ^0.3.3
@chainrails/vanilla: ^0.0.6

Summary by CodeRabbit

  • New Features

    • Cross-chain, multi-token payments with session creation and a web payment modal; SDK adds a payment() surface.
  • Examples

    • Added a Next.js demo and updated the Flappy Bird demo with an in-app "Save Me" revive checkout using the payment flow.
  • Documentation

    • Full payment configuration and usage guides; README clarifies browser vs server payment behavior.
  • Tests

    • Comprehensive test suite covering the payment surface and SDK integration.
  • Chores

    • Dev tooling: pre-commit checks, lint/prettier ignores, build script tweaks, env/gitignore scaffolding.

GoodyBoy301 and others added 30 commits February 25, 2026 20:58
- Added @chainrails/sdk as a dependency for payment processing.
- Introduced Payment module for handling cross-chain, multi-token payments.
- Implemented methods for quote discovery, intent management, and session handling.
- Updated SDK configuration to include payment settings.
- Created tests for Payment module functionalities and SDK integration.
- implement /session-token route to create StarkNet payment sessions
- integrate CHAINRAILS_API_KEY environment variable for payment feature
- log payment feature status on server start for clarity
feat(payment-server): add payment server with session token endpoint
…tion

  - Implemented Save Me modal UI with payment processing via StarkSDK
  - Added event handling for Save Me button, cancel, and close actions
  - Integrated Save Me modal with game state and revive logic
  - Updated main.js to handle Save Me flow, countdown, and resurrection
  - Enhanced CSS with countdown styling and overlay
  - Modified index.html to include Save Me assets and modal structure
  - Updated package-lock.json with new dependencies for StarkSDK and Chainrails
  - Added new asset 'saveme.png' for Save Me button
  - Extended starknet.ts to support Save Me payment interactions
feat(payment-web): add infinite flappy bird example with TypeScript and Vite setup
- Simplify modal input types and platform checks
- Remove deprecated session URL modal logic
- Integrate React Native modal opening method
- Update tests for platform-specific behavior
fix(eslint): exclude unused example from config
test(payment): add missing assertion for session token in payment test
…odal

- Introduce project structure with layout, page, and icons
- Add API route for creating payment sessions using Chainrails SDK
- Implement client-side payment modal integration
- Configure Tailwind CSS and global styles for the demo
- Include example icons for UI enhancement
- Add TypeScript configurations for project setup
- Change project name from 'Chainrails Next.js Demo' to 'Starkzap Next.js Demo'
- Replace Chainrails SDK usage with Starkzap SDK in API route
- Update frontend to initialize Starkzap SDK and handle payment session and modal
- Adjust configuration references from 'src/app/page.tsx'
- Modify package dependencies and scripts accordingly
- Improve clarity on destinationChain and token defaults in API
- Remove obsolete dependencies and comments for cleaner implementation
@0xsisyfos
Copy link
Contributor

0xsisyfos commented Mar 3, 2026

Thank you for this. Would you have a live demo of the flappy bird example with payment you have deployed?
Also, why was the mobile integration removed? We aim at keeping feature parity between mobile/web.

@GoodyBoy301
Copy link
Author

Thank you for this. Would you have a live demo of the flappy bird example with payment you have deployed? Also, why was the mobile integration removed? We aim at keeping feature parity between mobile/web.

Yes! You can try the Flappy Bird payment demo here: https://starkzap-flappy-bird.vercel.app/
For mobile integration, the current chainrails modal which is used on the web relies heavily on iframe communication using postmessages for security and other reasons. Replicating this iframe-based communication on mobile is currently not working as planned, so we've paused to explore alternative approaches.

@GoodyBoy301 GoodyBoy301 changed the title feat(payment): add ChainRails cross-chain payment module feat(payment): add Chainrails cross-chain payment module Mar 3, 2026
@0xsisyfos
Copy link
Contributor

Yes! You can try the Flappy Bird payment demo here: https://starkzap-flappy-bird.vercel.app/
I meant the a live demo of flappy bird with the payment implemented

@Darlington02
Copy link

I meant the a live demo of flappy bird with the payment implemented

yeah that's what he shared, I believe. The improvement shows up when one fails in the game. Rather than start again, you can simply pay with Chainrails to continue, does that make sense?

A deployed demo can be found here: https://starkzap-flappy-bird.vercel.app/
The updated code can be found in the flappy-bird repo

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds a new Chainrails-based Payments feature (types, Payment class, modal manager), exposes it via StarkZap SDK and config, includes docs, tests, examples (Next.js demo, Flappy Bird revive flow, server endpoint), and small tooling/ignore updates.

Changes

Cohort / File(s) Summary
Build & Tooling
/.husky/pre-commit, .prettierignore, eslint.config.mjs, package.json
Add pre-commit hook (NVM bootstrap + npm checks), ignore .next and next-env.d.ts, update tsc-alias flags, add @chainrails/* deps.
Public SDK surface
src/index.ts, src/sdk.ts, src/types/config.ts
Export payment surface; add StarkZap.payment(): Payment; add SDKConfig.payment?: PaymentConfig with runtime branching when config absent.
Payment implementation
src/payment/index.ts, src/payment/types.ts, src/payment/payment.ts, src/payment/modal.ts, src/types/payment-modules.d.ts
New Payment API, public types (sessions, quotes, intents, routing, chains, client info), Payment class, and PaymentModalManager implementing web modal flow; ambient types for @chainrails/vanilla.
Tests
tests/payment.test.ts
Large test suite covering Payment constants, sessions, quotes, intents, routing, chains, modal flow and SDK integration with mocks.
Docs & Navigation
README.md, mintlify-docs/**/payment.mdx, mintlify-docs/**/configuration.mdx, mintlify-docs/mint.json, mintlify-docs/docs.json, mintlify-docs/**/integrations/chainrails.mdx, mintlify-docs/**/overview.mdx
Add Cross-Chain Payments documentation, integration guide, configuration examples, and navigation entries.
Next.js example
examples/chainrails-nextjs-example/...
src/app/page.tsx, src/app/api/create-session/route.ts, next.config.ts, package.json, postcss.config.mjs, tsconfig*.json, README.md, .gitignore, .env.example, src/icons/*, src/app/layout.tsx, src/app/globals.css
Add Next 15 demo showing session creation and modal usage with client page and API route; Tailwind/TS config, assets, and env/example scaffolding.
Flappy Bird example
examples/flappy-bird/index.html, examples/flappy-bird/main.ts, examples/flappy-bird/public/css/main.css, examples/flappy-bird/public/js/main.js, examples/flappy-bird/README.md
Add "Save Me" revive UI and payment-driven revive flow using window.__saveMe(), timers, UI elements, animation and game logic adjustments.
Server example
examples/server/server.ts, examples/server/package.json, examples/server/.env.example
Optional server-side Chainrails integration guarded by CHAINRAILS_API_KEY; new /session-token endpoint; ESM package.json and local starkzap dependency.
Examples infra & misc files
examples/*/.gitignore, examples/*/.env.example, examples/*/package.json
Add example scaffolding files, environment samples, gitignores, and example package metadata.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client App
    participant SDK as StarkZap SDK
    participant Payment as Payment Manager
    participant Modal as Modal Manager
    participant Chainrails as Chainrails API

    Client->>SDK: payment()
    SDK->>Payment: instantiate(config)
    Client->>Payment: createSession(input)
    Payment->>Chainrails: POST /session (auth)
    Chainrails-->>Payment: { sessionToken, amount }
    Payment-->>Client: PaymentSessionOutput

    Client->>Payment: modal({sessionToken,...})
    Payment->>Modal: modal(input)
    Modal->>Modal: create element, bind callbacks, append & open
    Client->>Modal: handle.pay()
    Modal->>Chainrails: web modal interactions
    alt success
        Chainrails-->>Modal: success
        Modal-->>Client: Promise<true>
    else cancel/fail
        Modal-->>Client: Promise<false>
    end
    Modal->>Modal: cleanup DOM
Loading
sequenceDiagram
    participant Client as Client App
    participant Payment as Payment Manager
    participant Chainrails as Chainrails API

    Client->>Payment: getSessionQuotes(token)
    Payment->>Chainrails: GET /quotes?session=token
    Chainrails-->>Payment: Quote[]

    Client->>Payment: getBestQuote(input)
    Payment->>Chainrails: GET /quotes/best
    Chainrails-->>Payment: best Quote

    Client->>Payment: createSessionIntent(input)
    Payment->>Chainrails: POST /intents
    Chainrails-->>Payment: PaymentIntent

    Client->>Payment: triggerSessionProcessing(addr)
    Payment->>Chainrails: POST /intents/process
    Chainrails-->>Payment: { status, txHash }
    Payment-->>Client: TriggerProcessingOutput
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I dug a little payment lane,

Sessions, modals, docs — a shiny train.
Examples hop and tests hum clear,
SDK rails now carry cheer.
Hooray for carrots and a cross-chain gain! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: introducing a Payment module integrated with Chainrails for cross-chain payments, which aligns with the comprehensive changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

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

Copy link

@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: 16

Caution

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

⚠️ Outside diff range comments (1)
examples/flappy-bird/public/js/main.js (1)

546-553: ⚠️ Potential issue | 🟡 Minor

if (canUseSaveMe) is gating unrelated medal animation.

Because there’s no block, medal animation only runs when Save Me is available.

Proposed fix
       soundSwoosh.stop();
       soundSwoosh.play();
       $("#actions").transition({ y: '0px', opacity: 1}, 600, 'ease');
-      if (canUseSaveMe)
-
       //also animate in the MEDAL! WOO!
       if(wonmedal)
       {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/flappy-bird/public/js/main.js` around lines 546 - 553, The medal
animation is accidentally controlled by the preceding if (canUseSaveMe) because
there are no braces; fix by properly scoping the save-me conditional (wrap the
intended save-me statements in braces or terminate it so it’s a single
statement) so the wonmedal block (the wonmedal check and the $("#medal").css /
$("#medal").transition calls) executes independently; locate the if
(canUseSaveMe), the wonmedal condition, and the $("#medal") calls and adjust
braces/statement boundaries accordingly.
🧹 Nitpick comments (11)
examples/chainrails-nextjs-example/src/icons/NGFlag.tsx (2)

8-10: Avoid conflicting accessibility semantics on the SVG.

Line 8 hides the icon from assistive tech, while Line 9 assigns it an exposed image role. Keep one intent: decorative (aria-hidden only) or informative (role="img" + accessible label).

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

In `@examples/chainrails-nextjs-example/src/icons/NGFlag.tsx` around lines 8 - 10,
The SVG in the NGFlag component currently has conflicting accessibility
attributes (aria-hidden="true" and role="img"); decide whether the flag is
decorative or informative and make them consistent: for decorative icons keep
aria-hidden="true" and remove role="img"; for informative images remove
aria-hidden, keep role="img" and add an accessible label (aria-label or title)
to the SVG or expose a descriptive text prop on NGFlag so screen readers get a
name. Locate the SVG element inside the NGFlag component and apply the
corresponding change.

5-6: Normalize intrinsic SVG size to match rendered size.

width="800px"/height="800px" (Lines 5-6) is oversized relative to Line 11 (h-4 w-4). Consider removing intrinsic size or setting it to 24 for consistency and maintainability.

Also applies to: 11-11

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

In `@examples/chainrails-nextjs-example/src/icons/NGFlag.tsx` around lines 5 - 6,
The SVG in the NGFlag component uses oversized intrinsic width/height ("width"
and "height" attributes set to "800px") that conflict with its rendered classes
(className "h-4 w-4"); update the SVG by removing the explicit width/height
attributes or set them to 24 (e.g., width="24" height="24") so the intrinsic
size matches the rendered size, keeping the existing viewBox and paths intact;
locate the SVG element in the NGFlag component and adjust the width/height
attributes accordingly to normalize sizing.
examples/chainrails-nextjs-example/src/app/globals.css (1)

27-35: Minor Stylelint warnings.

Static analysis flagged a couple of minor style issues: empty lines before declarations and optimizeLegibility vs optimizelegibility (CSS values are case-insensitive, but Stylelint prefers lowercase). These are cosmetic and don't affect functionality.

✨ Optional fix to satisfy Stylelint
 :root {
   font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
   line-height: 1.5;
   font-weight: 400;
-
   color-scheme: light;
   color: `#213547`;
   background-color: `#ffffff`;
-
   font-synthesis: none;
-  text-rendering: optimizeLegibility;
+  text-rendering: optimizelegibility;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/chainrails-nextjs-example/src/app/globals.css` around lines 27 - 35,
Remove the stray empty line before the declarations in the CSS rule that
contains color-scheme, color, background-color, etc., and change the value
"optimizeLegibility" in the text-rendering declaration to lowercase
"optimizelegibility" to satisfy Stylelint's casing and blank-line rules; update
the block containing the properties (color-scheme, color, background-color,
font-synthesis, text-rendering, -webkit-font-smoothing, -moz-osx-font-smoothing)
so properties are consecutive with no extra blank line and the text-rendering
value is lowercase.
examples/chainrails-nextjs-example/package.json (1)

11-19: Consider moving build-time dependencies to devDependencies.

@tailwindcss/postcss and postcss are build-time tools and are typically placed in devDependencies rather than dependencies. This doesn't affect functionality but aligns with npm conventions.

Suggested change
   "dependencies": {
-    "@tailwindcss/postcss": "^4.1.18",
     "next": "^15.1.0",
-    "postcss": "^8.5.6",
     "react": "^19.2.1",
     "react-dom": "^19.2.1",
     "starkzap": "file:../..",
     "tailwindcss": "^4.1.18"
   },
   "devDependencies": {
+    "@tailwindcss/postcss": "^4.1.18",
     "@types/node": "^20",
     "@types/react": "^19",
     "@types/react-dom": "^19",
+    "postcss": "^8.5.6",
     "typescript": "^5"
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/chainrails-nextjs-example/package.json` around lines 11 - 19, Move
the build-time packages "@tailwindcss/postcss" and "postcss" out of the
"dependencies" section in package.json and add them to "devDependencies" with
the same version strings (e.g., "^4.1.18" and "^8.5.6") so they are installed
only for development/build. Update the package.json entries for "dependencies"
and "devDependencies" accordingly and run your package manager to refresh the
lockfile so the change is reflected in the project metadata.
examples/server/server.ts (2)

214-221: Hardcoded recipient address in demo endpoint.

The recipient address and amount are hardcoded, which is fine for demonstration purposes. Consider adding a comment noting these should be parameterized in production use.

Add clarifying comment
   try {
+    // Demo values - parameterize recipient and amount in production
     const session = await starkzap.payment().createSession({
       amount: "0.1",
       recipient:
         "0x0075597a61229d143Ffba493C9f8A8057ecCeeA7BFDDBFD8Aaf79AC8935205c0",
       destinationChain: PaymentChains.STARKNET,
       token: PaymentTokenSymbols.USDC,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/server/server.ts` around lines 214 - 221, The demo call to
starkzap.payment().createSession hardcodes amount and recipient (amount: "0.1",
recipient: "0x007559...5205c0", destinationChain: PaymentChains.STARKNET, token:
PaymentTokenSymbols.USDC); update the code by adding a clear inline comment
above this createSession call stating these values are hardcoded for demo only
and must be parameterized/validated in production (e.g., accept from request
body, validate address and amount) so reviewers know to replace createSession's
hardcoded recipient and amount in real deployments.

206-228: Inconsistent endpoint path and missing authentication.

Two observations:

  1. The /session-token endpoint doesn't follow the /api/ prefix convention used by other endpoints in this server.
  2. Unlike wallet endpoints, this endpoint has no authentication middleware. While acceptable for a demo, this should be documented or protected in production.
Suggested path consistency fix
 // Payment session endpoint (requires CHAINRAILS_API_KEY)
-app.get("/session-token", async (_, res) => {
+app.get("/api/session-token", async (_, res) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/server/server.ts` around lines 206 - 228, Rename the route path from
"/session-token" to "/api/session-token" for consistency and add the same
authentication middleware used by the wallet routes to the app.get handler
(e.g., app.get("/api/session-token", authMiddleware, async (...) => { ... }));
if you intentionally want this endpoint to be public for the demo, instead add
an explicit comment above the handler stating it is demo-only and must be
protected in production and/or require CHAINRAILS_API_KEY, referencing the
starkzap.payment().createSession call and PaymentChains.STARKNET /
PaymentTokenSymbols.USDC usage so reviewers know which endpoint to secure.
mintlify-docs/build/consumer-app-sdk/payment.mdx (2)

504-507: Avoid nested setTimeout with async callback in example code.

The setTimeout with an async callback can lead to unhandled promise rejections and is a common anti-pattern. Consider using a more robust polling pattern or event-based approach in the example.

Improved example with proper async handling
   if (success) {
     console.log("Payment successful!");
-    // Check status after a short delay
-    setTimeout(async () => {
-      const status = await checkPaymentStatus(session.sessionToken);
-      console.log("Intent status:", status);
-    }, 5000);
+    // Poll for status updates
+    await new Promise(resolve => setTimeout(resolve, 5000));
+    const status = await checkPaymentStatus(session.sessionToken);
+    console.log("Intent status:", status);
   } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mintlify-docs/build/consumer-app-sdk/payment.mdx` around lines 504 - 507, The
example currently uses setTimeout(async () => { const status = await
checkPaymentStatus(session.sessionToken); ... }, 5000) which can produce
unhandled promise rejections; replace this pattern with a dedicated async
polling function (e.g., pollPaymentStatus) that contains a try/catch around
await checkPaymentStatus(session.sessionToken), an explicit delay/sleep between
attempts, and a termination condition to stop polling when a terminal status is
reached; alternatively, call an async IIFE from setTimeout and handle errors
with .catch or try/catch instead of passing an async callback directly to
setTimeout.

526-535: Add proper type narrowing for error handling.

The error handling example accesses error.message without type checking, which could fail at runtime if error is not an Error instance.

Improved error handling example
 } catch (error) {
-  if (error.message.includes("API key")) {
+  const message = error instanceof Error ? error.message : String(error);
+  if (message.includes("API key")) {
     console.log("Check your Chainrails API key");
-  } else if (error.message.includes("network")) {
+  } else if (message.includes("network")) {
     console.log("Network error, try again");
   } else {
-    console.log("Unknown error:", error.message);
+    console.log("Unknown error:", message);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mintlify-docs/build/consumer-app-sdk/payment.mdx` around lines 526 - 535, The
catch block currently assumes the caught value is an Error and directly accesses
error.message; update the error handling to properly narrow the type by checking
`error instanceof Error` (or using `if (typeof error === "object" && error !==
null && "message" in error)`) before reading `error.message`, and fall back to a
safe string (e.g., String(error)) for logging; update the branches that inspect
`error.message` (the existing checks for "API key" and "network" and the final
"Unknown error" log) to use the narrowed message variable so runtime exceptions
from non-Error throws are avoided.
examples/chainrails-nextjs-example/src/app/api/create-session/route.ts (1)

11-11: Empty string fallback for API key may cause silent failures.

Using || "" when CHAINRAILS_API_KEY is undefined will pass an empty string to the SDK, which may fail later with a confusing error. Consider failing fast with a clear message.

🛡️ Proposed fix for explicit API key validation
+    const apiKey = process.env.CHAINRAILS_API_KEY;
+    if (!apiKey) {
+      console.error("CHAINRAILS_API_KEY environment variable is not set");
+      return NextResponse.json(
+        { error: "Payment service not configured" },
+        { status: 503 }
+      );
+    }
+
     const sdk = new StarkZap({
       network: "mainnet",
       payment: {
-        apiKey: process.env.CHAINRAILS_API_KEY || "",
+        apiKey,
         environment: "production",
       },
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/chainrails-nextjs-example/src/app/api/create-session/route.ts` at
line 11, The code currently falls back to an empty string for apiKey (apiKey:
process.env.CHAINRAILS_API_KEY || ""), which can cause silent failures; instead
read process.env.CHAINRAILS_API_KEY into a variable, validate it at startup or
at the top of the create-session route handler (e.g., in the module that builds
the SDK config), and throw or return a clear error if it's missing/empty (e.g.,
"Missing CHAINRAILS_API_KEY environment variable"); then pass the validated
variable (not an empty string) into the SDK config.
examples/chainrails-nextjs-example/src/app/page.tsx (1)

12-15: SDK instantiated on every render.

StarkZap is created inside the component body, causing a new instance on each render. Move initialization outside the component or wrap in useMemo.

♻️ Proposed fix using useMemo
 export default function Home() {
   const [loading, setLoading] = useState(false);
-  // Initialize SDK and payment modal
-  const sdk = new StarkZap({
-    network: "mainnet",
-  });
-  const payment = sdk.payment();
+
+  const payment = useMemo(() => {
+    const sdk = new StarkZap({ network: "mainnet" });
+    return sdk.payment();
+  }, []);

Also add the import:

-import { useState } from "react";
+import { useState, useMemo } from "react";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/chainrails-nextjs-example/src/app/page.tsx` around lines 12 - 15,
The component currently instantiates a new StarkZap on every render (see
StarkZap and sdk.payment()), so move the SDK creation out of the render path:
either create the StarkZap instance at module scope (top-level) or memoize it
inside the component with React's useMemo and import useMemo from 'react'; then
call sdk.payment() from that stable instance. Ensure you update the import list
to include useMemo if you choose the memo approach and reference the same
StarkZap variable used to derive payment().
tests/payment.test.ts (1)

1-503: Consider adding error/failure test cases.

The test suite thoroughly covers happy paths but lacks tests for error scenarios (e.g., API failures, invalid inputs, network errors). Adding a few negative test cases would improve resilience verification.

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

In `@tests/payment.test.ts` around lines 1 - 503, Add negative test cases to cover
API failures and invalid inputs by mocking crapi methods to reject or return
error payloads and asserting Payment methods and StarkZap.payment respond
correctly; for example, add tests that spyOn crapi.auth.getSessionToken to
reject and assert payment.createSession throws or returns a handled error, spyOn
crapi.quotes.getAll/getBestAcrossBridges to reject and assert
payment.getAllQuotes/getBestQuote propagate or handle errors, spyOn
crapi.intents.create/getById to reject and assert payment.createIntent/getIntent
handle failures, test getSupportedChains when crapi.chains.getSupported rejects,
and a StarkZap.payment negative case for browser vs non-browser by stubbing
globals and asserting thrown vs not thrown; locate these tests near existing
describes for createSession, getAllQuotes, createIntent, getIntent,
getSupportedChains, and the "StarkZap.payment()" describe and add expectations
for thrown errors or error return values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.husky/pre-commit:
- Around line 1-7: Add a bash shebang as the very first line so the hook always
runs under a defined shell (use /usr/bin/env to locate bash) before the existing
export NVM_DIR line, and ensure the .husky/pre-commit hook file is executable so
the subsequent export NVM_DIR and npm run typecheck / lint / prettier:check
commands run reliably.

In `@examples/chainrails-nextjs-example/README.md`:
- Around line 70-74: Update the README entry for "Starkzap SDK Reference" to
remove or correct the broken URL; locate the line containing the "Starkzap SDK
Reference" link in README.md and either delete that bullet or replace
"https://docs.starkzap.com" with the correct, accessible URL (or a note that the
documentation is unavailable), leaving the "Chainrails SDK Reference" and
"Chainrails API Reference" links unchanged.

In `@examples/chainrails-nextjs-example/src/app/api/create-session/route.ts`:
- Line 6: Validate the incoming JSON fields before calling createSession: check
that amount is present and a positive number (or numeric string convertible to a
number) and that recipient is present and a non-empty string (optionally
validate address format), and return a 400/Bad Request response if validation
fails; update the handler that destructures { destinationChain, token, amount,
recipient } to perform these checks and only call createSession when valid,
referencing the request handler in route.ts and the createSession invocation.

In `@examples/chainrails-nextjs-example/src/app/page.tsx`:
- Around line 174-182: The radio input with id "credit-card" is using a
controlled "checked" prop without any state or onChange handler; replace the
static checked attribute with defaultChecked for a static demo or wire it to
state (create a state variable and onChange handler for the "payment-method"
group) so React doesn't emit uncontrolled-to-controlled warnings—look for the
input element with id="credit-card" and name="payment-method" in the JSX and
either change checked to defaultChecked or implement state management in the
surrounding component (and update the className/props accordingly).
- Around line 125-127: The JSX uses a deprecated selected attribute on the
<option value="Ikeja">; remove the selected attribute and set the initial
selection on the parent <select> instead (use defaultValue="Ikeja" for
uncontrolled selects or manage value/onChange for a controlled select). Locate
the <option value="Ikeja" selected> instance and update its parent <select> to
use defaultValue="Ikeja" (or implement value/onChange) and delete the selected
attribute from the option.
- Around line 19-40: Wrap the fetch + session extraction and the
payment.modal(...).pay() call in a try/catch/finally: check response.ok after
fetch and throw or handle the error if not OK, validate that data.sessionToken
exists before calling payment.modal({ sessionToken: data.sessionToken, ...
}).pay(), and move setLoading(false) into finally so it always runs; reference
the fetch call to `${process.env.NEXT_PUBLIC_API_URL}/api/create-session`, the
extracted data.sessionToken, and the payment.modal(...).pay() invocation when
applying the changes.

In `@examples/chainrails-nextjs-example/src/icons/Tel.tsx`:
- Around line 12-18: The TelIcon component currently renders the check-circle
glyph (same path as CheckIcon); update TelIcon to use a telephone/phone SVG path
instead (or if the checkmark was intentional, rename the component/file to
CheckIcon). Locate the TelIcon component in the file and replace the <path> that
currently contains d="M8.5 11.5 11 14l4-4m6 2a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
with an appropriate phone/telephone path (or rename the component and its
exports to CheckIcon to match the existing path).

In `@examples/flappy-bird/index.html`:
- Around line 80-83: Replace the interactive <div> elements with semantic
<button> elements for the "save-me" and "replay" controls (keep the ids
"save-me" and "replay") so they are keyboard and assistive-tech accessible;
ensure each button has type="button" and an appropriate accessible name (use the
existing img alt or add an aria-label or visually hidden text) and preserve the
inner <img> markup and any event handlers/CSS selectors that reference those
ids.

In `@examples/flappy-bird/main.ts`:
- Around line 42-45: The payload.amount coming from response.json() is untyped;
before using it (e.g., when constructing modal input) coerce and validate it to
the expected string form: check payload.amount exists, convert it to a string
(e.g., String(payload.amount)) or use a safe default (e.g., '' or '0') if
null/undefined, and if you expect a numeric string optionally validate with
Number.isFinite(Number(payload.amount)) then format accordingly; apply this same
coercion/validation wherever payload.amount is read (the occurrences around the
sessionToken/amount assignment and the later block at lines 58-62) so the modal
always receives a properly typed string.
- Around line 31-40: The fetch in openSaveMeCheckout currently has no
timeout/abort; wrap the fetch call with an AbortController, pass
controller.signal to fetch, start a setTimeout that calls controller.abort()
after a reasonable timeout (e.g., 8–15s), clear the timeout after a successful
response, and catch the AbortError to throw or return a user-friendly timeout
error so the UI can unlock; apply the same pattern to the other checkout fetches
mentioned (the blocks around lines 90–96 and 121–123) to ensure all checkout
requests can be cancelled and cleaned up.

In `@examples/flappy-bird/public/css/main.css`:
- Line 145: There are stray blank lines immediately before CSS declarations that
trigger stylelint's declaration-empty-line-before rule; remove the empty line(s)
directly above the affected declarations (e.g., the -webkit-animation and
corresponding animation declarations) so that each property appears immediately
after the previous line within its rule, then re-run stylelint to verify the
errors are resolved.

In `@examples/flappy-bird/public/js/main.js`:
- Around line 370-375: The spacebar handler incorrectly blocks replay when a
"Save Me" option is available; change the early-return condition in the
ScoreScreen branch so it only returns when saveMeInProgress is true (not when
canUseSaveMe is true) so pressing space still triggers $("#replay").click() even
if canUseSaveMe is true; locate the block around currentstate ==
states.ScoreScreen and update the conditional that currently checks
saveMeInProgress || canUseSaveMe to only check saveMeInProgress.

In `@src/payment/modal.ts`:
- Around line 56-71: The closeWith path currently calls cleanup() before
settle(), allowing re-entrant callbacks to run without the settled guard; change
the order so the settled guard is set and resolve(value) is invoked before
performing cleanup. In practice, update closeWith (or adjust settle) so it calls
settle(value) first (which sets settled=true and resolve), then calls cleanup(),
keeping the existing settle, cleanup, and settled semantics intact.
- Around line 14-15: The modal code is forcing amount to the string "0" when
omitted (e.g., the object property amount: input.amount || "0"), which breaks
session-scoped payments; update the construction in src/payment/modal.ts to stop
defaulting to "0" — either assign amount: input.amount ?? undefined or only
include the amount property when input.amount is present (remove the || "0"
fallback) in both places where amount is set (the object near amount:
input.amount || "0" and the similar block around lines 77-80).

In `@src/sdk.ts`:
- Around line 515-525: The browser fallback in payment() silently constructs new
Payment({ apiKey: "" }) when this.config.payment is missing, causing confusing
runtime errors; update payment() (and its JSDoc for StarkZap if you prefer docs)
to instead throw a clear configuration error consistently by removing the
empty-apiKey fallback branch (i.e., stop creating Payment when isWebRuntime() is
true) and throw the same Error when !this.config.payment, referencing the
Payment class, the payment() method, this.config.payment, and isWebRuntime() so
reviewers can locate the change.

In `@tests/payment.test.ts`:
- Around line 304-321: The mocked response for crapi.intents.getAll uses total
as a string; change it to a number so the test matches the real API shape:
update the output object in the "listIntents" test (where MOCK_INTENT and
getAllSpy are defined and payment.listIntents is called) to use total: 1 (remove
the "1" as const) so getAllSpy.mockResolvedValue(output) returns total as a
number.

---

Outside diff comments:
In `@examples/flappy-bird/public/js/main.js`:
- Around line 546-553: The medal animation is accidentally controlled by the
preceding if (canUseSaveMe) because there are no braces; fix by properly scoping
the save-me conditional (wrap the intended save-me statements in braces or
terminate it so it’s a single statement) so the wonmedal block (the wonmedal
check and the $("#medal").css / $("#medal").transition calls) executes
independently; locate the if (canUseSaveMe), the wonmedal condition, and the
$("#medal") calls and adjust braces/statement boundaries accordingly.

---

Nitpick comments:
In `@examples/chainrails-nextjs-example/package.json`:
- Around line 11-19: Move the build-time packages "@tailwindcss/postcss" and
"postcss" out of the "dependencies" section in package.json and add them to
"devDependencies" with the same version strings (e.g., "^4.1.18" and "^8.5.6")
so they are installed only for development/build. Update the package.json
entries for "dependencies" and "devDependencies" accordingly and run your
package manager to refresh the lockfile so the change is reflected in the
project metadata.

In `@examples/chainrails-nextjs-example/src/app/api/create-session/route.ts`:
- Line 11: The code currently falls back to an empty string for apiKey (apiKey:
process.env.CHAINRAILS_API_KEY || ""), which can cause silent failures; instead
read process.env.CHAINRAILS_API_KEY into a variable, validate it at startup or
at the top of the create-session route handler (e.g., in the module that builds
the SDK config), and throw or return a clear error if it's missing/empty (e.g.,
"Missing CHAINRAILS_API_KEY environment variable"); then pass the validated
variable (not an empty string) into the SDK config.

In `@examples/chainrails-nextjs-example/src/app/globals.css`:
- Around line 27-35: Remove the stray empty line before the declarations in the
CSS rule that contains color-scheme, color, background-color, etc., and change
the value "optimizeLegibility" in the text-rendering declaration to lowercase
"optimizelegibility" to satisfy Stylelint's casing and blank-line rules; update
the block containing the properties (color-scheme, color, background-color,
font-synthesis, text-rendering, -webkit-font-smoothing, -moz-osx-font-smoothing)
so properties are consecutive with no extra blank line and the text-rendering
value is lowercase.

In `@examples/chainrails-nextjs-example/src/app/page.tsx`:
- Around line 12-15: The component currently instantiates a new StarkZap on
every render (see StarkZap and sdk.payment()), so move the SDK creation out of
the render path: either create the StarkZap instance at module scope (top-level)
or memoize it inside the component with React's useMemo and import useMemo from
'react'; then call sdk.payment() from that stable instance. Ensure you update
the import list to include useMemo if you choose the memo approach and reference
the same StarkZap variable used to derive payment().

In `@examples/chainrails-nextjs-example/src/icons/NGFlag.tsx`:
- Around line 8-10: The SVG in the NGFlag component currently has conflicting
accessibility attributes (aria-hidden="true" and role="img"); decide whether the
flag is decorative or informative and make them consistent: for decorative icons
keep aria-hidden="true" and remove role="img"; for informative images remove
aria-hidden, keep role="img" and add an accessible label (aria-label or title)
to the SVG or expose a descriptive text prop on NGFlag so screen readers get a
name. Locate the SVG element inside the NGFlag component and apply the
corresponding change.
- Around line 5-6: The SVG in the NGFlag component uses oversized intrinsic
width/height ("width" and "height" attributes set to "800px") that conflict with
its rendered classes (className "h-4 w-4"); update the SVG by removing the
explicit width/height attributes or set them to 24 (e.g., width="24"
height="24") so the intrinsic size matches the rendered size, keeping the
existing viewBox and paths intact; locate the SVG element in the NGFlag
component and adjust the width/height attributes accordingly to normalize
sizing.

In `@examples/server/server.ts`:
- Around line 214-221: The demo call to starkzap.payment().createSession
hardcodes amount and recipient (amount: "0.1", recipient: "0x007559...5205c0",
destinationChain: PaymentChains.STARKNET, token: PaymentTokenSymbols.USDC);
update the code by adding a clear inline comment above this createSession call
stating these values are hardcoded for demo only and must be
parameterized/validated in production (e.g., accept from request body, validate
address and amount) so reviewers know to replace createSession's hardcoded
recipient and amount in real deployments.
- Around line 206-228: Rename the route path from "/session-token" to
"/api/session-token" for consistency and add the same authentication middleware
used by the wallet routes to the app.get handler (e.g.,
app.get("/api/session-token", authMiddleware, async (...) => { ... })); if you
intentionally want this endpoint to be public for the demo, instead add an
explicit comment above the handler stating it is demo-only and must be protected
in production and/or require CHAINRAILS_API_KEY, referencing the
starkzap.payment().createSession call and PaymentChains.STARKNET /
PaymentTokenSymbols.USDC usage so reviewers know which endpoint to secure.

In `@mintlify-docs/build/consumer-app-sdk/payment.mdx`:
- Around line 504-507: The example currently uses setTimeout(async () => { const
status = await checkPaymentStatus(session.sessionToken); ... }, 5000) which can
produce unhandled promise rejections; replace this pattern with a dedicated
async polling function (e.g., pollPaymentStatus) that contains a try/catch
around await checkPaymentStatus(session.sessionToken), an explicit delay/sleep
between attempts, and a termination condition to stop polling when a terminal
status is reached; alternatively, call an async IIFE from setTimeout and handle
errors with .catch or try/catch instead of passing an async callback directly to
setTimeout.
- Around line 526-535: The catch block currently assumes the caught value is an
Error and directly accesses error.message; update the error handling to properly
narrow the type by checking `error instanceof Error` (or using `if (typeof error
=== "object" && error !== null && "message" in error)`) before reading
`error.message`, and fall back to a safe string (e.g., String(error)) for
logging; update the branches that inspect `error.message` (the existing checks
for "API key" and "network" and the final "Unknown error" log) to use the
narrowed message variable so runtime exceptions from non-Error throws are
avoided.

In `@tests/payment.test.ts`:
- Around line 1-503: Add negative test cases to cover API failures and invalid
inputs by mocking crapi methods to reject or return error payloads and asserting
Payment methods and StarkZap.payment respond correctly; for example, add tests
that spyOn crapi.auth.getSessionToken to reject and assert payment.createSession
throws or returns a handled error, spyOn
crapi.quotes.getAll/getBestAcrossBridges to reject and assert
payment.getAllQuotes/getBestQuote propagate or handle errors, spyOn
crapi.intents.create/getById to reject and assert payment.createIntent/getIntent
handle failures, test getSupportedChains when crapi.chains.getSupported rejects,
and a StarkZap.payment negative case for browser vs non-browser by stubbing
globals and asserting thrown vs not thrown; locate these tests near existing
describes for createSession, getAllQuotes, createIntent, getIntent,
getSupportedChains, and the "StarkZap.payment()" describe and add expectations
for thrown errors or error return values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18d149db-6fe4-4c0a-815f-c084c476676f

📥 Commits

Reviewing files that changed from the base of the PR and between cb102e0 and f8c75db.

⛔ Files ignored due to path filters (3)
  • examples/chainrails-nextjs-example/package-lock.json is excluded by !**/package-lock.json
  • examples/flappy-bird/public/assets/saveme.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (41)
  • .husky/pre-commit
  • .prettierignore
  • README.md
  • eslint.config.mjs
  • examples/chainrails-nextjs-example/.env.example
  • examples/chainrails-nextjs-example/.gitignore
  • examples/chainrails-nextjs-example/README.md
  • examples/chainrails-nextjs-example/next.config.ts
  • examples/chainrails-nextjs-example/package.json
  • examples/chainrails-nextjs-example/postcss.config.mjs
  • examples/chainrails-nextjs-example/src/app/api/create-session/route.ts
  • examples/chainrails-nextjs-example/src/app/globals.css
  • examples/chainrails-nextjs-example/src/app/layout.tsx
  • examples/chainrails-nextjs-example/src/app/page.tsx
  • examples/chainrails-nextjs-example/src/icons/ArrowDown.tsx
  • examples/chainrails-nextjs-example/src/icons/Check.tsx
  • examples/chainrails-nextjs-example/src/icons/NGFlag.tsx
  • examples/chainrails-nextjs-example/src/icons/Tel.tsx
  • examples/chainrails-nextjs-example/tsconfig.json
  • examples/chainrails-nextjs-example/tsconfig.node.json
  • examples/flappy-bird/README.md
  • examples/flappy-bird/index.html
  • examples/flappy-bird/main.ts
  • examples/flappy-bird/public/css/main.css
  • examples/flappy-bird/public/js/main.js
  • examples/server/.env.example
  • examples/server/package.json
  • examples/server/server.ts
  • mintlify-docs/build/consumer-app-sdk/configuration.mdx
  • mintlify-docs/build/consumer-app-sdk/payment.mdx
  • mintlify-docs/mint.json
  • package.json
  • src/index.ts
  • src/payment/index.ts
  • src/payment/modal.ts
  • src/payment/payment.ts
  • src/payment/types.ts
  • src/sdk.ts
  • src/types/config.ts
  • src/types/payment-modules.d.ts
  • tests/payment.test.ts

Comment on lines +14 to +15
amount: input.amount || "0",
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not force modal amount to "0" when omitted.

For session-scoped payments, amount can be intentionally absent. Defaulting to "0" risks incorrect display/processing.

Proposed fix
     const handle: PaymentModalHandle = {
       platform,
       sessionToken: input.sessionToken,
       pay: () => this.pay({ ...input, platform }),
-      amount: input.amount || "0",
+      ...(input.amount !== undefined ? { amount: input.amount } : {}),
     };
@@
       modal.setProps?.({
         sessionToken: input.sessionToken,
-        amount: input.amount || "0",
+        ...(input.amount !== undefined ? { amount: input.amount } : {}),
         isOpen: true,
         isPending: false,
         onCancel: () => closeWith(false),
         onSuccess: () => closeWith(true),
       });

Also applies to: 77-80

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

In `@src/payment/modal.ts` around lines 14 - 15, The modal code is forcing amount
to the string "0" when omitted (e.g., the object property amount: input.amount
|| "0"), which breaks session-scoped payments; update the construction in
src/payment/modal.ts to stop defaulting to "0" — either assign amount:
input.amount ?? undefined or only include the amount property when input.amount
is present (remove the || "0" fallback) in both places where amount is set (the
object near amount: input.amount || "0" and the similar block around lines
77-80).

Comment on lines +515 to +525
payment(): Payment {
if (!this.config.payment) {
if (isWebRuntime()) {
return new Payment({ apiKey: "" });
}
throw new Error(
"Payment is not configured. Provide a `payment` config with an `apiKey` when creating StarkZap."
);
}
return new Payment(this.config.payment);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Browser fallback with empty apiKey may cause confusing runtime errors.

The JSDoc states "Requires payment.apiKey to be set" but the implementation silently creates a Payment instance with an empty apiKey when running in a browser without configuration. This can lead to confusing Chainrails API errors at call time instead of a clear configuration error upfront.

Consider either:

  1. Throwing consistently in all runtimes when payment config is missing, or
  2. Documenting the browser fallback behavior explicitly in the JSDoc
Option 1: Consistent error throwing
   payment(): Payment {
     if (!this.config.payment) {
-      if (isWebRuntime()) {
-        return new Payment({ apiKey: "" });
-      }
       throw new Error(
         "Payment is not configured. Provide a `payment` config with an `apiKey` when creating StarkZap."
       );
     }
     return new Payment(this.config.payment);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
payment(): Payment {
if (!this.config.payment) {
if (isWebRuntime()) {
return new Payment({ apiKey: "" });
}
throw new Error(
"Payment is not configured. Provide a `payment` config with an `apiKey` when creating StarkZap."
);
}
return new Payment(this.config.payment);
}
payment(): Payment {
if (!this.config.payment) {
throw new Error(
"Payment is not configured. Provide a `payment` config with an `apiKey` when creating StarkZap."
);
}
return new Payment(this.config.payment);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sdk.ts` around lines 515 - 525, The browser fallback in payment()
silently constructs new Payment({ apiKey: "" }) when this.config.payment is
missing, causing confusing runtime errors; update payment() (and its JSDoc for
StarkZap if you prefer docs) to instead throw a clear configuration error
consistently by removing the empty-apiKey fallback branch (i.e., stop creating
Payment when isWebRuntime() is true) and throw the same Error when
!this.config.payment, referencing the Payment class, the payment() method,
this.config.payment, and isWebRuntime() so reviewers can locate the change.

Comment on lines +304 to +321
describe("listIntents", () => {
it("lists with pagination", async () => {
const output = {
intents: [MOCK_INTENT],
total: "1" as const,
limit: 10,
offset: 0,
};
const getAllSpy = vi
.spyOn(crapi.intents, "getAll")
.mockResolvedValue(output as never);

const result = await payment.listIntents({ limit: 10, offset: 0 });

expect(getAllSpy).toHaveBeenCalledWith({ limit: 10, offset: 0 });
expect(result.intents).toHaveLength(1);
});
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find relevant TypeScript files that might contain type definitions
fd -e ts -e tsx | head -20

Repository: keep-starknet-strange/starkzap

Length of output: 950


🏁 Script executed:

# Search for listIntents method definition and related types
rg "listIntents" -A 5 -B 2

Repository: keep-starknet-strange/starkzap

Length of output: 1815


🏁 Script executed:

# Search for interface definitions related to intents or pagination
rg "interface.*Intent" -A 10

Repository: keep-starknet-strange/starkzap

Length of output: 1653


🏁 Script executed:

# Search for the ListPaymentIntentsOutput type definition
rg "ListPaymentIntentsOutput" -A 10 -B 2

Repository: keep-starknet-strange/starkzap

Length of output: 2012


🏁 Script executed:

# Also search for ListPaymentIntentsInput to understand the interface
rg "ListPaymentIntentsInput" -A 10 -B 2

Repository: keep-starknet-strange/starkzap

Length of output: 2018


🏁 Script executed:

# Check the payment types file
fd payment -type f | grep -E "\.(ts|d\.ts)$" | grep -v test | grep -v node_modules

Repository: keep-starknet-strange/starkzap

Length of output: 244


🏁 Script executed:

# Search for AsyncResult type definition
rg "type AsyncResult" -A 5

Repository: keep-starknet-strange/starkzap

Length of output: 340


🏁 Script executed:

# Search for crapi object and intents.getAll definition
rg "crapi.*intents" -B 5 -A 10 | head -100

Repository: keep-starknet-strange/starkzap

Length of output: 5198


🏁 Script executed:

# Look for where crapi is imported or defined
rg "crapi" src/payment/ -l

Repository: keep-starknet-strange/starkzap

Length of output: 117


🏁 Script executed:

# Search for crapi import in payment files
rg "import.*crapi" src/payment/

Repository: keep-starknet-strange/starkzap

Length of output: 211


🏁 Script executed:

# Also check if crapi is defined in a types file or config
rg "crapi" --type ts -B 2 -A 2 | grep -E "(export|declare|from|import)" | head -30

Repository: keep-starknet-strange/starkzap

Length of output: 2300


🏁 Script executed:

# Search for node_modules chainrails SDK type definitions
find node_modules/@chainrails -name "*.d.ts" 2>/dev/null | head -20

Repository: keep-starknet-strange/starkzap

Length of output: 56


🏁 Script executed:

# Look for package.json to see what version is being used
rg "chainrails" package.json

Repository: keep-starknet-strange/starkzap

Length of output: 143


🏁 Script executed:

# Search for any documentation or comments about the intent list response
rg "intents.*getAll" -A 20 -B 5

Repository: keep-starknet-strange/starkzap

Length of output: 4213


🏁 Script executed:

# Search for pagination or total field definitions in the codebase
rg "total.*:" -B 2 -A 2 | grep -E "(interface|type|total)" | head -40

Repository: keep-starknet-strange/starkzap

Length of output: 2707


🌐 Web query:

Chainrails SDK @chainrails/sdk intents.getAll response type total field

💡 Result:

crapi.intents.getAll(...) returns a paginated list object where the total field is a number (the total count of intents matching the query across all pages, not just the length of the current data/items array). This isn’t explicitly documented in the public SDK/API reference pages, but the SDK ships “full TypeScript type definitions,” so the authoritative answer in your installed version is to inspect the exported response type in node_modules/@chainrails/sdk (or via your IDE hover). [1]

Source(s):
[1] Chainrails SDK Reference – Introduction (mentions full TypeScript type definitions) https://docs.chainrails.io/sdk-reference/introduction


🏁 Script executed:

# Search for other pagination examples in the codebase
rg "limit.*offset" -B 5 -A 5 | head -100

Repository: keep-starknet-strange/starkzap

Length of output: 729


🏁 Script executed:

# Check if there are any type hints or JSDoc comments about intents response
rg "@chainrails" -A 20 -B 5 | head -150

Repository: keep-starknet-strange/starkzap

Length of output: 6946


Fix mock total field type: should be a number, not a string.

The crapi.intents.getAll() API returns total as a number representing the total count of intents across all pages. Change total: "1" as const to total: 1.

const output = {
  intents: [MOCK_INTENT],
- total: "1" as const,
+ total: 1,
  limit: 10,
  offset: 0,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/payment.test.ts` around lines 304 - 321, The mocked response for
crapi.intents.getAll uses total as a string; change it to a number so the test
matches the real API shape: update the output object in the "listIntents" test
(where MOCK_INTENT and getAllSpy are defined and payment.listIntents is called)
to use total: 1 (remove the "1" as const) so getAllSpy.mockResolvedValue(output)
returns total as a number.

GoodyBoy301 and others added 5 commits March 4, 2026 11:04
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…route.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- wrap fetch call in try-catch block for robustness
- check response status before parsing JSON
- validate presence of sessionToken in response data
- log errors to console for debugging
…rove UI

- Introduce paymentMethod state to manage selected payment option
- Add defaultValue to city select dropdown for better UX
- Synchronize radio button checked states with paymentMethod
- Update Tel icon SVG with a new path for better appearance
Copy link

@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: 4

🧹 Nitpick comments (2)
examples/chainrails-nextjs-example/README.md (2)

32-45: Add language specifier to project structure code block.

The fenced code block displaying the directory structure is missing a language identifier, which is flagged by markdownlint.

📝 Suggested improvement
-```
+```plaintext
 src/
 ├── app/
 │   ├── api/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/chainrails-nextjs-example/README.md` around lines 32 - 45, Update
the fenced code block in README.md that displays the project directory tree (the
block that starts with ``` and contains "src/" and its subfolders) to include a
language specifier such as plaintext (i.e., change the opening fence from ``` to
```plaintext) so markdownlint recognizes it as a formatted code block.

17-20: Add language specifier to environment variables code block.

The fenced code block is missing a language identifier. Adding one improves rendering and follows markdown best practices.

📝 Suggested improvement
-```
+```bash
 NEXT_PUBLIC_API_URL=http://localhost:3000
 NEXT_PUBLIC_CHAINRAILS_API_KEY=your_api_key_here

Additionally, consider adding a note about where to obtain the Chainrails API key (e.g., link to Chainrails dashboard or registration page) to help new users get started more easily.

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @examples/chainrails-nextjs-example/README.md around lines 17 - 20, Update
the fenced environment variables block to include a language specifier (e.g.,
"bash") so the snippet with NEXT_PUBLIC_API_URL and
NEXT_PUBLIC_CHAINRAILS_API_KEY renders correctly, and add a short note after the
block indicating where to obtain the Chainrails API key (for example, a link or
reference to the Chainrails dashboard/registration page) so users know how to
get the value for NEXT_PUBLIC_CHAINRAILS_API_KEY.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @examples/chainrails-nextjs-example/src/app/page.tsx:

  • Around line 149-153: The label's htmlFor ("phone-input-3") does not match the
    input's id ("phone-input"), breaking accessibility; update one so they match
    (either change the label htmlFor to "phone-input" or rename the input id to
    "phone-input-3") in the component where the Phone Number label and input are
    defined (ensure the unique attributes htmlFor on the and id on the
    corresponding are identical).
  • Around line 18-59: The pay() function unconditionally opens the Chainrails
    crypto modal; update it to read the selected payment method state (e.g.,
    selectedPaymentMethod or paymentMethod) and branch: only call
    payment.modal(...).pay() when the selected method is the crypto option
    (PaymentChains.STARKNET / token flow), otherwise route to the appropriate
    handlers for "Payment on delivery" or "Paypal" (e.g., call your COD handler or
    redirect to PayPal) and skip the crypto modal; modify pay() and any invocation
    sites that assume crypto so the modal is only used for the crypto payment path.
  • Around line 21-23: The fetch URL is built directly from NEXT_PUBLIC_API_URL
    which can be undefined; update the fetch in page.tsx (the call that posts to
    "/api/create-session") to guard the env var and fall back to a same-origin path:
    compute a base URL from NEXT_PUBLIC_API_URL and if it's falsy use the
    same-origin relative path so the final URL is either
    ${NEXT_PUBLIC_API_URL}/api/create-session or /api/create-session, then pass
    that URL into the existing fetch call.
  • Around line 251-264: The PayPal radio input with id "paypal" is missing its
    value binding and state handlers and the label's htmlFor is incorrectly
    "paypal-2"; update the input to have value="paypal" and wire it to the same
    state as the other radios (use the paymentMethod state for checked and the
    setPaymentMethod onChange handler used by the other payment inputs), and change
    the label's htmlFor from "paypal-2" to "paypal" so the label correctly targets
    the input.

Nitpick comments:
In @examples/chainrails-nextjs-example/README.md:

  • Around line 32-45: Update the fenced code block in README.md that displays the
    project directory tree (the block that starts with and contains "src/" and its subfolders) to include a language specifier such as plaintext (i.e., change the opening fence from to ```plaintext) so markdownlint recognizes it as a
    formatted code block.
  • Around line 17-20: Update the fenced environment variables block to include a
    language specifier (e.g., "bash") so the snippet with NEXT_PUBLIC_API_URL and
    NEXT_PUBLIC_CHAINRAILS_API_KEY renders correctly, and add a short note after the
    block indicating where to obtain the Chainrails API key (for example, a link or
    reference to the Chainrails dashboard/registration page) so users know how to
    get the value for NEXT_PUBLIC_CHAINRAILS_API_KEY.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `e10debd5-82e0-48f8-8306-7d4e99de1c9e`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bcba91a6ac6d1571a140d60b1466e99f6438247d and 0d722265cdffea8bbbcc344f06f0684a2e5bfc81.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `examples/chainrails-nextjs-example/README.md`
* `examples/chainrails-nextjs-example/src/app/api/create-session/route.ts`
* `examples/chainrails-nextjs-example/src/app/page.tsx`
* `examples/chainrails-nextjs-example/src/icons/Tel.tsx`
* `examples/flappy-bird/public/js/main.js`

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* examples/chainrails-nextjs-example/src/icons/Tel.tsx
* examples/chainrails-nextjs-example/src/app/api/create-session/route.ts

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +18 to +59
async function pay() {
setLoading(true);
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/create-session`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
destinationChain: PaymentChains.STARKNET,
token: PaymentTokenSymbols.USDC,
recipient:
"0x0075597a61229d143Ffba493C9f8A8057ecCeeA7BFDDBFD8Aaf79AC8935205c0",
amount: "0.99",
}),
}
);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

if (!data.sessionToken) {
throw new Error("Session token not found in response");
}

const paid = await payment
.modal({
sessionToken: data.sessionToken,
amount: "0.99",
})
.pay();

console.log(paid ? "Payment Successful" : "Payment Failed");
} catch (error) {
console.error("Payment failed:", error);
} finally {
setLoading(false);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Selected payment method is ignored during submission.

Line 309 always triggers pay(), but inside pay() (Line 46) you always execute the Chainrails modal. If the user selects “Payment on delivery” or “Paypal”, the flow still runs crypto checkout.

♻️ Proposed fix
   async function pay() {
+    if (paymentMethod !== "credit-card") {
+      console.error(`Payment method "${paymentMethod}" is not implemented yet.`);
+      return;
+    }
+
     setLoading(true);
     try {

Also applies to: 309-315

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

In `@examples/chainrails-nextjs-example/src/app/page.tsx` around lines 18 - 59,
The pay() function unconditionally opens the Chainrails crypto modal; update it
to read the selected payment method state (e.g., selectedPaymentMethod or
paymentMethod) and branch: only call payment.modal(...).pay() when the selected
method is the crypto option (PaymentChains.STARKNET / token flow), otherwise
route to the appropriate handlers for "Payment on delivery" or "Paypal" (e.g.,
call your COD handler or redirect to PayPal) and skip the crypto modal; modify
pay() and any invocation sites that assume crypto so the modal is only used for
the crypto payment path.

Comment on lines +21 to +23
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/create-session`,
{
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard NEXT_PUBLIC_API_URL to avoid malformed fetch URLs.

At Line 22, if NEXT_PUBLIC_API_URL is unset, the request becomes undefined/api/create-session and fails at runtime. Use a same-origin fallback.

♻️ Proposed fix
-      const response = await fetch(
-        `${process.env.NEXT_PUBLIC_API_URL}/api/create-session`,
+      const apiBase = (process.env.NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, "");
+      const response = await fetch(
+        `${apiBase}/api/create-session`,
         {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/create-session`,
{
const apiBase = (process.env.NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, "");
const response = await fetch(
`${apiBase}/api/create-session`,
{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/chainrails-nextjs-example/src/app/page.tsx` around lines 21 - 23,
The fetch URL is built directly from NEXT_PUBLIC_API_URL which can be undefined;
update the fetch in page.tsx (the call that posts to "/api/create-session") to
guard the env var and fall back to a same-origin path: compute a base URL from
NEXT_PUBLIC_API_URL and if it's falsy use the same-origin relative path so the
final URL is either `${NEXT_PUBLIC_API_URL}/api/create-session` or
`/api/create-session`, then pass that URL into the existing fetch call.

Copy link

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mintlify-docs/build/consumer-app-sdk/payment.mdx`:
- Around line 477-479: The polling call is using session.sessionToken (a session
token) instead of the payment intent identifier, causing
getIntent/checkPaymentStatus to fail; update the call in the polling block to
pass the real intent ID (for example session.intentId or session.intent.id
depending on the session shape) into checkPaymentStatus/getIntent so the
functions receive the actual intent identifier rather than session.sessionToken,
and verify the session object property name in the code that creates the session
before changing the call.
- Around line 222-230: The example for getQuotesFromAllBridges includes an
unsupported field excludeBridges; remove that key from the input object passed
to getQuotesFromAllBridges so the call matches the documented SDK input type.
Locate the call to payment.getQuotesFromAllBridges in the snippet and delete the
excludeBridges: "CCTP" line, keeping sourceChain, destinationChain, tokenIn,
tokenOut, amount, and recipient as shown.
- Line 225: The example currently embeds a real-looking ERC20 address in the
tokenIn field which triggers secret scans; update the docs sample to use a
sanitized placeholder (e.g. a clearly fake address or constant placeholder
string) instead of the real-looking literal—modify the tokenIn value in the
example where the tokenIn property is defined so it reads as a placeholder token
address (e.g. "0x000...000" or "YOUR_TOKEN_ADDRESS") to avoid false positives
from static analysis.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 817da1d0-dda9-4e99-a4d9-a3fac738d40b

📥 Commits

Reviewing files that changed from the base of the PR and between 0d72226 and 0ba1d24.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • mintlify-docs/build/consumer-app-sdk/configuration.mdx
  • mintlify-docs/build/consumer-app-sdk/integrations/chainrails.mdx
  • mintlify-docs/build/consumer-app-sdk/overview.mdx
  • mintlify-docs/build/consumer-app-sdk/payment.mdx
  • mintlify-docs/docs.json
  • mintlify-docs/mint.json
✅ Files skipped from review due to trivial changes (1)
  • mintlify-docs/build/consumer-app-sdk/integrations/chainrails.mdx

Comment on lines +222 to +230
const quotes = await payment.getQuotesFromAllBridges({
sourceChain: "ETHEREUM",
destinationChain: "STARKNET",
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
tokenOut: "0x053c91253...", // USDC on Starknet
amount: "1000",
recipient: "0xrecipient...",
excludeBridges: "CCTP", // Optional: exclude specific bridges
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove unsupported excludeBridges from getQuotesFromAllBridges example.

Line 229 uses excludeBridges, but the documented SDK input type for this method does not include that field. This example will mislead users into a type/runtime mismatch.

Suggested doc fix
 const quotes = await payment.getQuotesFromAllBridges({
   sourceChain: "ETHEREUM",
   destinationChain: "STARKNET",
   tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
   tokenOut: "0x053c91253...", // USDC on Starknet
   amount: "1000",
   recipient: "0xrecipient...",
-  excludeBridges: "CCTP",  // Optional: exclude specific bridges
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const quotes = await payment.getQuotesFromAllBridges({
sourceChain: "ETHEREUM",
destinationChain: "STARKNET",
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
tokenOut: "0x053c91253...", // USDC on Starknet
amount: "1000",
recipient: "0xrecipient...",
excludeBridges: "CCTP", // Optional: exclude specific bridges
});
const quotes = await payment.getQuotesFromAllBridges({
sourceChain: "ETHEREUM",
destinationChain: "STARKNET",
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
tokenOut: "0x053c91253...", // USDC on Starknet
amount: "1000",
recipient: "0xrecipient...",
});
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 225-225: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

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

In `@mintlify-docs/build/consumer-app-sdk/payment.mdx` around lines 222 - 230, The
example for getQuotesFromAllBridges includes an unsupported field
excludeBridges; remove that key from the input object passed to
getQuotesFromAllBridges so the call matches the documented SDK input type.
Locate the call to payment.getQuotesFromAllBridges in the snippet and delete the
excludeBridges: "CCTP" line, keeping sourceChain, destinationChain, tokenIn,
tokenOut, amount, and recipient as shown.

const quotes = await payment.getQuotesFromAllBridges({
sourceChain: "ETHEREUM",
destinationChain: "STARKNET",
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a placeholder token address to avoid secret-scanner false positives.

Line 225 currently uses a full literal address and is already being flagged by static analysis. Prefer a sanitized placeholder in docs examples.

Suggested doc fix
-  tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
+  tokenIn: "0xUSDC_ON_ETHEREUM...", // USDC
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
tokenIn: "0xUSDC_ON_ETHEREUM...", // USDC
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 225-225: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

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

In `@mintlify-docs/build/consumer-app-sdk/payment.mdx` at line 225, The example
currently embeds a real-looking ERC20 address in the tokenIn field which
triggers secret scans; update the docs sample to use a sanitized placeholder
(e.g. a clearly fake address or constant placeholder string) instead of the
real-looking literal—modify the tokenIn value in the example where the tokenIn
property is defined so it reads as a placeholder token address (e.g.
"0x000...000" or "YOUR_TOKEN_ADDRESS") to avoid false positives from static
analysis.

Comment on lines +477 to +479
setTimeout(async () => {
const status = await checkPaymentStatus(session.sessionToken);
console.log("Intent status:", status);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix status polling to use an actual intent ID, not sessionToken.

Line 478 passes session.sessionToken into getIntent(...). A session token is not an intent identifier, so this flow is incorrect and will fail for consumers who copy/paste it.

Suggested doc fix
-      const status = await checkPaymentStatus(session.sessionToken);
+      // Use a real payment intent ID returned by your intent creation flow.
+      const status = await checkPaymentStatus("<intent_id>");
       console.log("Intent status:", status);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTimeout(async () => {
const status = await checkPaymentStatus(session.sessionToken);
console.log("Intent status:", status);
setTimeout(async () => {
// Use a real payment intent ID returned by your intent creation flow.
const status = await checkPaymentStatus("<intent_id>");
console.log("Intent status:", status);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mintlify-docs/build/consumer-app-sdk/payment.mdx` around lines 477 - 479, The
polling call is using session.sessionToken (a session token) instead of the
payment intent identifier, causing getIntent/checkPaymentStatus to fail; update
the call in the polling block to pass the real intent ID (for example
session.intentId or session.intent.id depending on the session shape) into
checkPaymentStatus/getIntent so the functions receive the actual intent
identifier rather than session.sessionToken, and verify the session object
property name in the code that creates the session before changing the call.

- update htmlFor attribute to match input id for accessibility
- set radio button value to 'paypal' and manage checked state with React
- add onChange handler to update paymentMethod state

refactor(modal): move cleanup call to closeWith function

- ensure cleanup is called when closing modal to prevent leaks

fix(sdk): update comment regarding API key requirement

- clarify that payment.apiKey is needed in SDK config unless on browser
- improve documentation for cross-environment usage
Copy link

@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: 1

♻️ Duplicate comments (4)
examples/chainrails-nextjs-example/src/app/page.tsx (2)

18-59: ⚠️ Potential issue | 🟠 Major

Honor the selected payment method before invoking Chainrails.

pay() currently routes every submission through the crypto modal, even when the user selected “Payment on delivery” or “Paypal”.

♻️ Proposed fix
 async function pay() {
+  if (paymentMethod !== "credit-card") {
+    // TODO: Replace with real handlers for each payment path.
+    console.error(`Payment method "${paymentMethod}" is not implemented yet.`);
+    return;
+  }
+
   setLoading(true);
   try {
     const response = await fetch(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/chainrails-nextjs-example/src/app/page.tsx` around lines 18 - 59,
The pay function currently always opens Chainrails crypto modal; modify pay() to
honor the user's selected payment method (e.g., a state variable like
selectedPaymentMethod) by branching before calling payment.modal: if the method
is a crypto option (match against PaymentChains/PaymentTokenSymbols or a crypto
enum value) then invoke payment.modal({ sessionToken: ..., amount }).pay(),
otherwise call the appropriate handler for other methods (e.g., trigger PayPal
flow or mark order for Payment on Delivery) and only setLoading/unsetLoading
around the chosen flow; ensure you still validate the API response
(data.sessionToken) only when needed for the crypto path.

21-23: ⚠️ Potential issue | 🟠 Major

Add a safe fallback when NEXT_PUBLIC_API_URL is missing.

If the env var is unset, the request URL becomes invalid and session creation fails at runtime.

♻️ Proposed fix
-      const response = await fetch(
-        `${process.env.NEXT_PUBLIC_API_URL}/api/create-session`,
+      const apiBase = (process.env.NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, "");
+      const sessionUrl = apiBase ? `${apiBase}/api/create-session` : "/api/create-session";
+      const response = await fetch(
+        sessionUrl,
         {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/chainrails-nextjs-example/src/app/page.tsx` around lines 21 - 23,
The fetch call that builds the request URL using process.env.NEXT_PUBLIC_API_URL
can produce an invalid URL if the env var is missing; update the code around the
fetch in page.tsx to use a safe fallback (e.g., window.location.origin or a
default base URL) when NEXT_PUBLIC_API_URL is undefined or empty, or explicitly
throw a clear error before calling fetch; ensure you reference the same endpoint
("/api/create-session") and the fetch invocation so callers still reach the
correct route even when the env var is not set.
src/payment/modal.ts (2)

56-60: ⚠️ Potential issue | 🟠 Major

Make closeWith idempotent before running cleanup.

Line 70 executes cleanup() even after already settled; concurrent onCancel/onSuccess paths can trigger repeated cleanup and re-entrant close behavior.

Proposed fix
-      const settle = (value: boolean): void => {
-        if (settled) return;
+      const settle = (value: boolean): boolean => {
+        if (settled) return false;
         settled = true;
         resolve(value);
+        return true;
       };
@@
       const closeWith = (value: boolean): void => {
-        settle(value);
+        if (!settle(value)) return;
         cleanup();
       };

Also applies to: 68-71

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

In `@src/payment/modal.ts` around lines 56 - 60, The closeWith/settle flow is not
idempotent: ensure closeWith checks the shared `settled` flag and returns
immediately if already settled before calling `cleanup()` or performing any
closing logic; keep the existing `settle` implementation (which sets `settled =
true` then `resolve(value)`) but invoke it only after the early-return guard in
`closeWith`, and apply the same idempotency guard to the other duplicate close
path (the lines corresponding to the second close/settle block) so concurrent
onCancel/onSuccess handlers cannot run cleanup or re-enter close logic more than
once.

14-15: ⚠️ Potential issue | 🟠 Major

Do not coerce missing amount to "0" in modal handle/props.

Line 14 and Line 79 still force "0" when amount is intentionally omitted. That breaks optional/session-scoped flows.

Proposed fix
     const handle: PaymentModalHandle = {
       platform,
       sessionToken: input.sessionToken,
       pay: () => this.pay({ ...input, platform }),
-      amount: input.amount || "0",
+      ...(input.amount !== undefined ? { amount: input.amount } : {}),
     };
@@
       modal.setProps?.({
         sessionToken: input.sessionToken,
-        amount: input.amount || "0",
+        ...(input.amount !== undefined ? { amount: input.amount } : {}),
         isOpen: true,
         isPending: false,
         onCancel: () => closeWith(false),
         onSuccess: () => closeWith(true),
       });

Also applies to: 77-80

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

In `@src/payment/modal.ts` around lines 14 - 15, The code is forcing a default
string "0" for the amount field (amount: input.amount || "0"), which breaks
optional/session-scoped flows; change the assignment to preserve an omitted
value (e.g., amount: input.amount or amount: input.amount ?? undefined) and
remove the hardcoded "0" default in both occurrences (the object being built
around amount and the modal props/handle population at lines ~77-80). Also
ensure the related type/interface (e.g., PaymentModalProps / the payload type)
marks amount as optional so callers can legitimately omit it without runtime
coercion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/payment/modal.ts`:
- Around line 65-66: The cleanup function reference being cleared must be the
same as the one set: create a named constant (e.g., const cleanup = () =>
closeWith(false)) and use that same variable when calling the setter and when
calling this.clearActiveModalCleanup(cleanup) so identity matches and
activeModalCleanup can be cleared; update the code around the
activeModalCleanup/setter usage (where () => closeWith(false) is currently
inlined) to use the shared cleanup reference instead.

---

Duplicate comments:
In `@examples/chainrails-nextjs-example/src/app/page.tsx`:
- Around line 18-59: The pay function currently always opens Chainrails crypto
modal; modify pay() to honor the user's selected payment method (e.g., a state
variable like selectedPaymentMethod) by branching before calling payment.modal:
if the method is a crypto option (match against
PaymentChains/PaymentTokenSymbols or a crypto enum value) then invoke
payment.modal({ sessionToken: ..., amount }).pay(), otherwise call the
appropriate handler for other methods (e.g., trigger PayPal flow or mark order
for Payment on Delivery) and only setLoading/unsetLoading around the chosen
flow; ensure you still validate the API response (data.sessionToken) only when
needed for the crypto path.
- Around line 21-23: The fetch call that builds the request URL using
process.env.NEXT_PUBLIC_API_URL can produce an invalid URL if the env var is
missing; update the code around the fetch in page.tsx to use a safe fallback
(e.g., window.location.origin or a default base URL) when NEXT_PUBLIC_API_URL is
undefined or empty, or explicitly throw a clear error before calling fetch;
ensure you reference the same endpoint ("/api/create-session") and the fetch
invocation so callers still reach the correct route even when the env var is not
set.

In `@src/payment/modal.ts`:
- Around line 56-60: The closeWith/settle flow is not idempotent: ensure
closeWith checks the shared `settled` flag and returns immediately if already
settled before calling `cleanup()` or performing any closing logic; keep the
existing `settle` implementation (which sets `settled = true` then
`resolve(value)`) but invoke it only after the early-return guard in
`closeWith`, and apply the same idempotency guard to the other duplicate close
path (the lines corresponding to the second close/settle block) so concurrent
onCancel/onSuccess handlers cannot run cleanup or re-enter close logic more than
once.
- Around line 14-15: The code is forcing a default string "0" for the amount
field (amount: input.amount || "0"), which breaks optional/session-scoped flows;
change the assignment to preserve an omitted value (e.g., amount: input.amount
or amount: input.amount ?? undefined) and remove the hardcoded "0" default in
both occurrences (the object being built around amount and the modal
props/handle population at lines ~77-80). Also ensure the related type/interface
(e.g., PaymentModalProps / the payload type) marks amount as optional so callers
can legitimately omit it without runtime coercion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ff763716-13a4-4cf0-81f9-f050100d8aa3

📥 Commits

Reviewing files that changed from the base of the PR and between 0ba1d24 and ef3e67b.

📒 Files selected for processing (3)
  • examples/chainrails-nextjs-example/src/app/page.tsx
  • src/payment/modal.ts
  • src/sdk.ts

Comment on lines +65 to +66
this.clearActiveModalCleanup(cleanup);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use the same function reference for set/clear active cleanup.

Line 73 stores () => closeWith(false), but Line 65 clears with cleanup; identity never matches at Line 104, so activeModalCleanup can stay stale.

Proposed fix
+      let activeCleanup!: () => void;
+
       const cleanup = (): void => {
         modal.close();
         modal.remove();
-        this.clearActiveModalCleanup(cleanup);
+        this.clearActiveModalCleanup(activeCleanup);
       };
@@
-      this.setActiveModalCleanup(() => closeWith(false));
+      activeCleanup = () => closeWith(false);
+      this.setActiveModalCleanup(activeCleanup);

Also applies to: 73-73, 103-105

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

In `@src/payment/modal.ts` around lines 65 - 66, The cleanup function reference
being cleared must be the same as the one set: create a named constant (e.g.,
const cleanup = () => closeWith(false)) and use that same variable when calling
the setter and when calling this.clearActiveModalCleanup(cleanup) so identity
matches and activeModalCleanup can be cleared; update the code around the
activeModalCleanup/setter usage (where () => closeWith(false) is currently
inlined) to use the shared cleanup reference instead.

…nd modal integration

- Introduce server-side session creation with API key protection
- Add client-side modal opening instructions with code snippets
- Include advanced intent creation example for control
- Emphasize security best practices to prevent API key exposure
- Update related resource links for comprehensive documentation
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.

3 participants