Skip to content

Fix KiloClaw early bird checkout to link payments to existing Stripe customer#773

Open
kilo-code-bot[bot] wants to merge 4 commits intomainfrom
session/agent_1aceb2ba-6240-4b30-8f20-76f4267cadf7
Open

Fix KiloClaw early bird checkout to link payments to existing Stripe customer#773
kilo-code-bot[bot] wants to merge 4 commits intomainfrom
session/agent_1aceb2ba-6240-4b30-8f20-76f4267cadf7

Conversation

@kilo-code-bot
Copy link
Contributor

@kilo-code-bot kilo-code-bot bot commented Mar 3, 2026

Summary

  • Replace the static Stripe Payment Link (buy.stripe.com/...) with a server-side Stripe Checkout Session that passes the user's existing stripe_customer_id, preventing duplicate/guest Stripe accounts
  • Add createEarlybirdCheckoutSession tRPC mutation to the kiloclawRouter, following the same patterns used by Kilo Pass and other checkout flows
  • Update the early bird page to call the new mutation instead of opening an external Stripe link

Problem

The early bird checkout used a static Stripe Payment Link with only prefilled_email, which is cosmetic and does not link the payment to the user's existing Stripe customer record. This caused:

  • Payments appearing under a guest/duplicate Stripe customer
  • The Stripe link from the KiloCode admin panel not working
  • Users ending up with 2 Stripe accounts
CleanShot.2026-03-03.at.10.40.01.mp4

This is different than before as it remembers my Stripe user.

NOTE: This requires two new environment variables I've already added to Vercel:

STRIPE_KILOCLAW_EARLYBIRD_PRICE_ID and STRIPE_KILOCLAW_EARLYBIRD_COUPON_ID

Changes

src/routers/kiloclaw-router.ts

Added createEarlybirdCheckoutSession mutation that:

  • Validates the user has a stripe_customer_id
  • Reads STRIPE_KILOCLAW_EARLYBIRD_PRICE_ID from env vars (the Stripe price configured for the early bird offer)
  • Creates a Stripe Checkout Session with customer: stripeCustomerId — the key fix
  • Follows the same pattern as kiloPass.createCheckoutSession (billing address collection, tax ID collection, customer_update, metadata with type and kiloUserId)

src/app/(app)/claw/earlybird/page.tsx

  • Replaced static buy.stripe.com link with a tRPC mutation call (trpc.kiloclaw.createEarlybirdCheckoutSession)
  • Button now redirects via window.location.href after checkout session creation (same pattern as Kilo Pass)
  • Added loading state and error handling via sonner toast

.env.test

  • Added STRIPE_KILOCLAW_EARLYBIRD_PRICE_ID test env var

Setup required

The STRIPE_KILOCLAW_EARLYBIRD_PRICE_ID environment variable must be set in Vercel/production to the Stripe Price ID for the early bird subscription product.


Built for Brendan by Kilo for Slack

…ession

Replace the static Stripe Payment Link with a server-side Stripe
Checkout Session that passes the user's existing stripe_customer_id.
This prevents duplicate/guest Stripe customer accounts from being
created during early bird purchases.

createEarlybirdCheckoutSession: baseProcedure
.output(z.object({ url: z.url().nullable() }))
.mutation(async ({ ctx }) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

[WARNING]: No guard against duplicate early bird subscriptions

Unlike createCheckoutSession in the Kilo Pass router, which checks for an existing active subscription before creating a checkout session (lines 914-920), this endpoint has no such check. A user could:

  1. Click the button multiple times (the frontend isPending guard only prevents concurrent clicks, not sequential ones after a failure/navigation)
  2. Call the tRPC endpoint directly to create multiple checkout sessions
  3. End up with multiple active early bird subscriptions on the same Stripe customer

Consider adding a check for an existing active kiloclaw-earlybird subscription on the Stripe customer before creating a new checkout session, similar to how kilo-pass-router.ts does it.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is not a subscription.

@kilo-code-bot
Copy link
Contributor Author

kilo-code-bot bot commented Mar 3, 2026

Code Review Summary

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 3
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
src/routers/kiloclaw-router.ts 403 Metadata is set on the Checkout Session, but charge.succeeded handler reads from paymentIntent.metadata. Stripe does not copy session metadata to the PaymentIntent — use payment_intent_data: { metadata: {...} } instead.

WARNING

File Line Issue
src/lib/stripe.ts 432 isKiloclawEarlybird check will never match because metadata is on the Checkout Session, not the PaymentIntent. Earlybird charges will fall through to "Unknown charge type" warning.
src/routers/kiloclaw-router.ts 368 No guard against duplicate early bird purchases — a user can create multiple checkout sessions and pay multiple times.
src/routers/kiloclaw-router.ts 388 No visible server-side fulfillment after payment succeeds — the earlybird purchase is skipped in charge.succeeded but no checkout.session.completed handler processes it either.
Files Reviewed (4 files)
  • .env.test - 0 issues
  • src/app/(app)/claw/earlybird/page.tsx - 0 issues
  • src/lib/stripe.ts - 1 issue
  • src/routers/kiloclaw-router.ts - 3 issues

Fix these issues in Kilo Cloud

const couponId = getEnvVariable('STRIPE_KILOCLAW_EARLYBIRD_COUPON_ID');

const session = await stripe.checkout.sessions.create({
mode: 'payment',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

[WARNING]: No visible server-side fulfillment after payment

This creates a one-time mode: 'payment' checkout session, but there's no webhook handler or server-side logic to actually provision the earlybird benefit after payment completes. The user is redirected to success_url but nothing records the purchase or activates the 6-month compute benefit.

If fulfillment is handled manually or via a separate system (e.g., Stripe dashboard automation), consider adding a code comment documenting that. Otherwise, a checkout.session.completed or charge.succeeded handler for type: 'kiloclaw-earlybird' is needed to complete the flow.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fulfillment doesn't exist yet so this is known I think.

We could optionally add the user to a cohort via a webhook but I think we should do that as a follow up PR

@olearycrew olearycrew requested a review from pandemicsyn March 3, 2026 15:50
},
success_url: `${APP_URL}/claw?earlybird_checkout=success`,
cancel_url: `${APP_URL}/claw/earlybird?checkout=cancelled`,
metadata: {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

[CRITICAL]: Metadata is set on the Checkout Session, but the charge.succeeded webhook handler in stripe.ts checks paymentIntent.metadata.type. Stripe does not automatically copy session-level metadata to the PaymentIntent.

This means isKiloclawEarlybird in handleSuccessfulChargeWithPayment will never be true, and the charge will fall through to the "Unknown charge type" warning branch, triggering a Sentry warning on every earlybird purchase.

Other checkout flows in this codebase (e.g. auto-topup-setup at stripe.ts:829) correctly use payment_intent_data: { metadata: { ... } } to propagate metadata to the PaymentIntent.

Fix: add payment_intent_data to the session creation:

payment_intent_data: {
  metadata: {
    type: 'kiloclaw-earlybird',
    kiloUserId: ctx.user.id,
  },
},

You may still want session-level metadata for your own lookup purposes, but the PaymentIntent metadata is what the webhook reads.

const isAutoTopUpSetup = paymentIntent?.metadata?.type === 'auto-topup-setup';

// KiloClaw earlybird purchases are handled separately - not a credit top-up
const isKiloclawEarlybird = paymentIntent?.metadata?.type === 'kiloclaw-earlybird';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

[WARNING]: This check will never match unless the metadata is propagated to the PaymentIntent.

The metadata in kiloclaw-router.ts is set on the Checkout Session object (via stripe.checkout.sessions.create({ metadata: { type: 'kiloclaw-earlybird' } })), but Stripe does not automatically copy session-level metadata to the underlying PaymentIntent. To make this guard work, the checkout session creation needs to use payment_intent_data: { metadata: { type: 'kiloclaw-earlybird', kiloUserId: ctx.user.id } } instead of (or in addition to) top-level metadata.

Without this fix, earlybird charges will fall through to the else branch and trigger the "Unknown charge type" Sentry warning.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant