Skip to content

feat: Add KasperoPay payment app for Kaspa cryptocurrency#27954

Open
kasperolabs wants to merge 1 commit intocalcom:mainfrom
kasperolabs:feat/kasperopay-payment-app
Open

feat: Add KasperoPay payment app for Kaspa cryptocurrency#27954
kasperolabs wants to merge 1 commit intocalcom:mainfrom
kasperolabs:feat/kasperopay-payment-app

Conversation

@kasperolabs
Copy link

What does this PR do?

Adds KasperoPay as a payment app, enabling Cal.com users to accept Kaspa (KAS) cryptocurrency payments for paid bookings. This follows the same pattern as existing crypto payment apps (Alby, BTCPay Server).

KasperoPay (https://kaspa-store.com/) is a payment processor for the Kaspa blockchain — the fastest proof-of-work cryptocurrency with 1-second block times.

How it works

  1. Merchant installs the KasperoPay app and enters their merchant ID
  2. Merchant enables KAS payments on an event type and sets a price
  3. When a booking requires payment, a payment session is created via KasperoPay API
  4. User sees a payment page supporting multiple Kaspa wallets (Kasware, Kastle, Keystone, QR code)
  5. On payment confirmation, a webhook notifies Cal.com to confirm the booking automatically

Visual Demo

Image Demo:

pay-screen
confirm-cal com-side
meeting-scheduled

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A — app is self-contained with its own README.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  1. Sign up for a free KasperoPay merchant account at https://kaspa-store.com/merchant to get a merchant ID (format: kpm_xxxxx)
  2. Install the KasperoPay app from the app store and enter your merchant ID
  3. Create an event type with payment required, select KasperoPay, set a price in KAS
  4. Book the event as a guest — you'll be redirected to the payment page
  5. Pay with a Kaspa wallet (Kasware Chrome extension is easiest for testing)
  6. After payment confirms on the blockchain (~1 second), the booking is automatically confirmed

No special environment variables required beyond the standard Cal.com setup. The app communicates with the KasperoPay API at kaspa-store.com.

Checklist

@kasperolabs kasperolabs requested a review from a team as a code owner February 14, 2026 15:13
@CLAassistant
Copy link

CLAassistant commented Feb 14, 2026

CLA assistant check
All committers have signed the CLA.

@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label Feb 14, 2026
Copy link
Contributor

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

Choose a reason for hiding this comment

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

9 issues found across 28 files

Prompt for AI agents (all issues)

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


<file name="packages/app-store/kasperopay/api/webhook.ts">

<violation number="1" location="packages/app-store/kasperopay/api/webhook.ts:106">
P1: Rule violated: **Avoid Logging Sensitive Information**

Logging the raw Zod parse error from credential validation can expose sensitive `webhook_secret` and `merchant_id` values in application logs. Zod errors include received input values in their output. Log only a generic message instead.

(Based on your team's feedback about not logging raw ZodError objects as they may expose sensitive data.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/app-store/kasperopay/lib/PaymentService.ts">

<violation number="1" location="packages/app-store/kasperopay/lib/PaymentService.ts:16">
P3: Environment variable access is inlined here instead of going through the centralized config/constants layer required by project policy, which reduces consistency and validation of env-derived values.</violation>

<violation number="2" location="packages/app-store/kasperopay/lib/PaymentService.ts:83">
P1: Rule violated: **Avoid Logging Sensitive Information**

The full KasperoPay API response is logged including the `token` field, which is a sensitive payment session authentication token. Only log non-sensitive fields (e.g., `session_id`, `error`) or sanitize the response before logging to avoid exposing secrets in logs.</violation>
</file>

<file name="apps/web/components/apps/kasperopay/KasperoPayPaymentComponent.tsx">

<violation number="1" location="apps/web/components/apps/kasperopay/KasperoPayPaymentComponent.tsx:98">
P2: Fallback flow opens a new tab after setting `isPaying(true)` but never clears it, leaving the UI stuck in a loading spinner if the user closes or abandons the payment tab.</violation>
</file>

<file name="packages/app-store/kasperopay/components/KasperoPriceComponent.tsx">

<violation number="1" location="packages/app-store/kasperopay/components/KasperoPriceComponent.tsx:12">
P3: User-facing tooltip strings are hardcoded ("loading...", "Price unavailable", and the USD format), bypassing the app’s localization system. This will display English-only text in a multilingual UI.</violation>

<violation number="2" location="packages/app-store/kasperopay/components/KasperoPriceComponent.tsx:19">
P2: The KAS/USD rate fetch does not validate the HTTP response and falls back to a hardcoded 0.1 rate, which can display a misleading USD value when the API fails or returns an unexpected payload. In a financial context, this should error out instead of using an arbitrary rate.</violation>
</file>

<file name="packages/app-store/kasperopay/pages/setup/index.tsx">

<violation number="1" location="packages/app-store/kasperopay/pages/setup/index.tsx:2">
P2: Pages Router page imports `useRouter` from `next/navigation` (App Router API), which is unsupported in `pages/` and can break routing at runtime. Use `next/router` here.</violation>

<violation number="2" location="packages/app-store/kasperopay/pages/setup/index.tsx:34">
P2: User-facing strings are hardcoded instead of using the existing localization hook, which makes the setup page non-translatable.</violation>
</file>

<file name="packages/app-store/kasperopay/components/EventTypeAppSettingsInterface.tsx">

<violation number="1" location="packages/app-store/kasperopay/components/EventTypeAppSettingsInterface.tsx:77">
P2: The controlled input clears when price is 0, which prevents entering decimal values that start with 0 (e.g., `0.5`). Allow 0 to remain as the value instead of converting it to `undefined`.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

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


const parseCredentials = kasperopayCredentialKeysSchema.safeParse(key);
if (!parseCredentials.success) {
console.error("Invalid credentials:", parseCredentials.error);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 14, 2026

Choose a reason for hiding this comment

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

P1: Rule violated: Avoid Logging Sensitive Information

Logging the raw Zod parse error from credential validation can expose sensitive webhook_secret and merchant_id values in application logs. Zod errors include received input values in their output. Log only a generic message instead.

(Based on your team's feedback about not logging raw ZodError objects as they may expose sensitive data.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/kasperopay/api/webhook.ts, line 106:

<comment>Logging the raw Zod parse error from credential validation can expose sensitive `webhook_secret` and `merchant_id` values in application logs. Zod errors include received input values in their output. Log only a generic message instead.

(Based on your team's feedback about not logging raw ZodError objects as they may expose sensitive data.) </comment>

<file context>
@@ -0,0 +1,185 @@
+
+    const parseCredentials = kasperopayCredentialKeysSchema.safeParse(key);
+    if (!parseCredentials.success) {
+      console.error("Invalid credentials:", parseCredentials.error);
+      throw new HttpCode({ statusCode: 500, message: "Credentials not valid" });
+    }
</file context>
Fix with Cubic

const initData: KasperoPayInitResponse = await initResponse.json();

if (!initData.success || !initData.session_id) {
log.error("KasperoPay: Failed to initialize payment", safeStringify(initData));
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 14, 2026

Choose a reason for hiding this comment

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

P1: Rule violated: Avoid Logging Sensitive Information

The full KasperoPay API response is logged including the token field, which is a sensitive payment session authentication token. Only log non-sensitive fields (e.g., session_id, error) or sanitize the response before logging to avoid exposing secrets in logs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/kasperopay/lib/PaymentService.ts, line 83:

<comment>The full KasperoPay API response is logged including the `token` field, which is a sensitive payment session authentication token. Only log non-sensitive fields (e.g., `session_id`, `error`) or sanitize the response before logging to avoid exposing secrets in logs.</comment>

<file context>
@@ -0,0 +1,192 @@
+      const initData: KasperoPayInitResponse = await initResponse.json();
+
+      if (!initData.success || !initData.session_id) {
+        log.error("KasperoPay: Failed to initialize payment", safeStringify(initData));
+        throw new Error(initData.error || "Failed to initialize KasperoPay session");
+      }
</file context>
Fix with Cubic

});
} else {
// Fallback: open payment page directly
window.open(
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 14, 2026

Choose a reason for hiding this comment

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

P2: Fallback flow opens a new tab after setting isPaying(true) but never clears it, leaving the UI stuck in a loading spinner if the user closes or abandons the payment tab.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/components/apps/kasperopay/KasperoPayPaymentComponent.tsx, line 98:

<comment>Fallback flow opens a new tab after setting `isPaying(true)` but never clears it, leaving the UI stuck in a loading spinner if the user closes or abandons the payment tab.</comment>

<file context>
@@ -0,0 +1,212 @@
+        });
+      } else {
+        // Fallback: open payment page directly
+        window.open(
+          `${KASPEROPAY_API}/pay/session/${session.session_id}?token=${session.token}`,
+          "_blank"
</file context>
Fix with Cubic

try {
// Fetch current KAS/USD rate
const response = await fetch("https://kaspa-store.com/api/store/price/kas");
const data = await response.json();
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 14, 2026

Choose a reason for hiding this comment

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

P2: The KAS/USD rate fetch does not validate the HTTP response and falls back to a hardcoded 0.1 rate, which can display a misleading USD value when the API fails or returns an unexpected payload. In a financial context, this should error out instead of using an arbitrary rate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/kasperopay/components/KasperoPriceComponent.tsx, line 19:

<comment>The KAS/USD rate fetch does not validate the HTTP response and falls back to a hardcoded 0.1 rate, which can display a misleading USD value when the API fails or returns an unexpected payload. In a financial context, this should error out instead of using an arbitrary rate.</comment>

<file context>
@@ -0,0 +1,51 @@
+      try {
+        // Fetch current KAS/USD rate
+        const response = await fetch("https://kaspa-store.com/api/store/price/kas");
+        const data = await response.json();
+        const kasToUsd = data.kasToUsd || 0.1;
+        const usdValue = price * kasToUsd;
</file context>
Fix with Cubic

e.preventDefault();

if (!merchantId.startsWith("kpm_")) {
showToast("Merchant ID must start with 'kpm_'", "error");
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 14, 2026

Choose a reason for hiding this comment

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

P2: User-facing strings are hardcoded instead of using the existing localization hook, which makes the setup page non-translatable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/kasperopay/pages/setup/index.tsx, line 34:

<comment>User-facing strings are hardcoded instead of using the existing localization hook, which makes the setup page non-translatable.</comment>

<file context>
@@ -0,0 +1,139 @@
+    e.preventDefault();
+    
+    if (!merchantId.startsWith("kpm_")) {
+      showToast("Merchant ID must start with 'kpm_'", "error");
+      return;
+    }
</file context>
Fix with Cubic

@@ -0,0 +1,139 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 14, 2026

Choose a reason for hiding this comment

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

P2: Pages Router page imports useRouter from next/navigation (App Router API), which is unsupported in pages/ and can break routing at runtime. Use next/router here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/kasperopay/pages/setup/index.tsx, line 2:

<comment>Pages Router page imports `useRouter` from `next/navigation` (App Router API), which is unsupported in `pages/` and can break routing at runtime. Use `next/router` here.</comment>

<file context>
@@ -0,0 +1,139 @@
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
</file context>
Fix with Cubic

setAppData("currency", currency);
}
}}
value={price && price > 0 ? price : undefined}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 14, 2026

Choose a reason for hiding this comment

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

P2: The controlled input clears when price is 0, which prevents entering decimal values that start with 0 (e.g., 0.5). Allow 0 to remain as the value instead of converting it to undefined.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/kasperopay/components/EventTypeAppSettingsInterface.tsx, line 77:

<comment>The controlled input clears when price is 0, which prevents entering decimal values that start with 0 (e.g., `0.5`). Allow 0 to remain as the value instead of converting it to `undefined`.</comment>

<file context>
@@ -0,0 +1,129 @@
+                    setAppData("currency", currency);
+                  }
+                }}
+                value={price && price > 0 ? price : undefined}
+              />
+            </div>
</file context>
Fix with Cubic

@@ -0,0 +1,51 @@
import React from "react";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 14, 2026

Choose a reason for hiding this comment

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

P3: User-facing tooltip strings are hardcoded ("loading...", "Price unavailable", and the USD format), bypassing the app’s localization system. This will display English-only text in a multilingual UI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/kasperopay/components/KasperoPriceComponent.tsx, line 12:

<comment>User-facing tooltip strings are hardcoded ("loading...", "Price unavailable", and the USD format), bypassing the app’s localization system. This will display English-only text in a multilingual UI.</comment>

<file context>
@@ -0,0 +1,51 @@
+};
+
+export function KasperoPriceComponent({ displaySymbol, price, formattedPrice }: KasperoPriceComponentProps) {
+  const [fiatValue, setFiatValue] = React.useState<string>("loading...");
+
+  React.useEffect(() => {
</file context>
Fix with Cubic


const log = logger.getSubLogger({ prefix: ["payment-service:kasperopay"] });

const KASPEROPAY_API_URL = process.env.KASPEROPAY_API_URL || "https://kaspa-store.com";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 14, 2026

Choose a reason for hiding this comment

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

P3: Environment variable access is inlined here instead of going through the centralized config/constants layer required by project policy, which reduces consistency and validation of env-derived values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/kasperopay/lib/PaymentService.ts, line 16:

<comment>Environment variable access is inlined here instead of going through the centralized config/constants layer required by project policy, which reduces consistency and validation of env-derived values.</comment>

<file context>
@@ -0,0 +1,192 @@
+
+const log = logger.getSubLogger({ prefix: ["payment-service:kasperopay"] });
+
+const KASPEROPAY_API_URL = process.env.KASPEROPAY_API_URL || "https://kaspa-store.com";
+
+interface KasperoPayInitResponse {
</file context>
Fix with Cubic

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

Labels

community Created by Linear-GitHub Sync size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants