Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions .specs/impact-referrals.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ BCP 14 [RFC 2119] [RFC 8174] keywords apply only when they appear in all capital
after the referral reward was earned. Annual subscription issuances and already-created issuances are not eligible.
- **Kilo Pass bonus-like issuance item**: Kilo Pass issuance item of kind `bonus`, `promo_first_month_50pct`, or
`referral_bonus`. At most one bonus-like item may exist for an issuance.
- **Referral launch cutoff**: UTC instant configured for the Kilo Pass referral launch. First-time monthly subscribers
who started before the cutoff keep legacy month-2 welcome promo behavior; subscribers starting at or after the cutoff
receive only the first-month welcome promo.
- **Kilo Pass welcome-month issuance**: Monthly base issuance that receives the 50% welcome promo under the
subscription-start schedule in `.specs/kilo-pass.md`. It is streak month 1 for starts before `2026-10-09T00:00:00Z`
and streak month 2 for starts at or after that cutoff.

## Overview

Expand Down Expand Up @@ -535,8 +535,8 @@ application, and Kilo Pass redeems after local referral bonus allocation.
114. A qualified Kilo Pass referral conversion MUST grant one Kilo Pass referral bonus reward to the referrer unless
cap-limited or disqualified.

115. Kilo Pass first-time monthly subscribers MUST keep the first-month welcome promo when otherwise eligible. Referral
rewards MUST NOT replace, reduce, or retroactively alter the source conversion's first-month welcome issuance.
115. Kilo Pass first-time monthly subscribers MUST keep the welcome promo when otherwise eligible. Referral rewards MUST
NOT replace, reduce, or retroactively alter the beneficiary's welcome-month issuance.

116. Kilo Pass referral reward value MUST be snapshotted at conversion as 50% of the referee's eligible monthly tier.
Later tier changes by either beneficiary MUST NOT change the reward amount.
Expand All @@ -551,7 +551,8 @@ application, and Kilo Pass redeems after local referral bonus allocation.
rewards represent up to five future monthly referral bonus applications.

120. At eligible Kilo Pass base monthly issuance time, if the beneficiary has one or more pending unexpired Kilo Pass
referral rewards and no permanent application blocker applies, the system MUST consume exactly one reward.
referral rewards, no permanent application blocker applies, and the issuance is not the beneficiary's welcome-month
issuance (Rule 123), the system MUST consume exactly one reward.

121. At most one pending Kilo Pass referral reward MUST be consumed per eligible monthly Kilo Pass base issuance.

Expand All @@ -563,6 +564,8 @@ application, and Kilo Pass redeems after local referral bonus allocation.

123. A Kilo Pass referral reward MUST apply only to an eligible monthly base issuance after the reward is earned. It MUST
NOT apply retroactively to the source conversion's base issuance or to any already-created issuance.
If the issuance is the beneficiary's welcome-month issuance, the reward MUST stay pending for a later issuance.
This applies to referee and referrer rewards.

124. A consumed Kilo Pass referral reward MUST create a distinct `referral_bonus` Kilo Pass issuance item.

Expand All @@ -587,9 +590,10 @@ application, and Kilo Pass redeems after local referral bonus allocation.
rewards but only an annual Kilo Pass subscription, rewards remain pending until an eligible monthly subscription is
available or the rewards expire.

132. Kilo Pass welcome bonus behavior MUST change at referral launch: first-time monthly subscribers who started before
the referral launch cutoff keep legacy month-2 promo eligibility; subscribers starting at or after the cutoff MUST
receive only the first-month welcome promo.
132. The monthly welcome schedule MUST follow the subscription-start cutoff in `.specs/kilo-pass.md`.
Eligible first-time subscribers starting before `2026-10-09T00:00:00Z` receive the welcome promo only in month 1.
Those starting at or after that cutoff receive the welcome promo only in month 2.
Returning subscribers MUST always use the normal monthly ramp.

### Shared Reward Granting

Expand Down Expand Up @@ -813,6 +817,12 @@ application, and Kilo Pass redeems after local referral bonus allocation.

## Changelog

### 2026-10-08 -- Keep the welcome month free of referral rewards

Defined the welcome-month issuance from the Kilo Pass subscription-start schedule. A pending referral reward now waits
for the next eligible issuance when the current issuance is the beneficiary's welcome month, for both referees and
referrers. This keeps the month-2 welcome promo for subscribers starting at or after `2026-10-09T00:00:00Z`.

### 2026-08-19 -- Retire the KiloClaw referral program

Removed the KiloClaw Advocate referral program. New KiloClaw referral touches, participants, conversions, decisions, and
Expand Down
30 changes: 23 additions & 7 deletions .specs/kilo-pass.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,14 @@ with welcome-promo overrides. Yearly subscriptions use a flat 50% monthly bonus.
Bonus-decision paths MUST clamp streak to at least `1` before applying the ramp.
14. Yearly cadence MUST use a flat monthly bonus of 50% of the monthly price and MUST NOT use the monthly ramp or
welcome-promo branch.
15. An eligible first-time monthly subscriber MUST receive a 50% bonus in streak month 1 instead of the monthly ramp
value.
16. An eligible first-time monthly subscriber whose subscription start is strictly before `2026-05-07T00:00:00Z` MUST
receive a 50% bonus in streak month 2 instead of the monthly ramp value.
15. For eligible first-time monthly subscribers started strictly before `2026-10-09T00:00:00Z`, streak month 1 MUST use
the 50% welcome bonus, month 2 MUST use the normal 10% ramp, and month 3 MUST use the normal 15% ramp.
This includes subscriptions started before May 7, 2026; no two-month welcome-promo grandfathering remains.
16. For eligible first-time monthly subscribers started at or after `2026-10-09T00:00:00Z`, streak month 1 MUST use the
normal 5% ramp, month 2 MUST use the 50% welcome bonus, and month 3 MUST use the normal 15% ramp.
The subscription start timestamp MUST determine the schedule, with no persisted schedule flag.
Missing or invalid subscription starts MUST conservatively use the legacy month-1 schedule.
Non-first-time subscribers MUST always use the normal monthly ramp, regardless of subscription start.
17. From streak month 3 onward, and in any month where the welcome promo does not apply, monthly cadence MUST use the
monthly ramp value.
18. New welcome-promo grants MUST use the `bonus` issuance-item kind with a 50% applied percent.
Expand Down Expand Up @@ -414,9 +418,8 @@ The following behavior or stronger guarantees are not implemented by current cod
8. Explicit ended-state streak reset for reactivated store subscriptions.
9. Guaranteed expiry for every granted bonus credit.
10. Independent durable audit recording for every failed provider or issuance operation.
11. Retirement of the grandfathered streak-month-2 promo branch after no eligible pre-cutoff subscriptions remain.
12. Store-provider welcome-promo anti-abuse signals equivalent to Stripe fingerprint claims.
13. Atomic prevention of concurrent or repeated active Kilo Pass purchases by the same user. Duplicate-card Rules 51-55
11. Store-provider welcome-promo anti-abuse signals equivalent to Stripe fingerprint claims.
12. Atomic prevention of concurrent or repeated active Kilo Pass purchases by the same user. Duplicate-card Rules 51-55
intentionally exclude same-user purchases.

## Adjacent Spec Compatibility
Expand All @@ -431,6 +434,19 @@ promo or Kilo Pass referral conversion.

## Changelog

### 2026-10-08 -- Timestamp-based monthly welcome bonus cutoff

- Eligible first-time monthly starts strictly before October 9, 2026 at midnight UTC receive 50%, 10%, then 15%.
- Starts at or after the cutoff receive 5%, 50%, then 15%; returning subscribers always use the normal ramp.
- Removed the stored schedule flag and its unshipped migration; subscription start is the only schedule authority.
- Removed the former pre-May-7 two-month welcome bonus, while keeping missing/invalid starts on the month-1 schedule.
- Kept the regular monthly ramp, yearly bonuses, payment eligibility, and usage unlock unchanged.
- Updated subscription offers, ramp previews, and KiloClaw upsells to show the new schedule.
- Kept yearly upsell text at 50% every month and limited welcome callouts to the applicable promo window.
- Kept the selected welcome month in ramp totals after the welcome callout disappears.
- Pending referral rewards skip the welcome-month issuance and apply to the next eligible issuance
(`.specs/impact-referrals.md` Rule 123).

### 2026-06-08 -- Yearly duplicate-card enforcement

- First paid yearly purchases are now subject to the same 24-hour cross-account card-reuse restriction as first paid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Badge } from '@/components/ui/badge';
import { KiloPassIcon } from '@/components/icons/KiloPassIcon';
import { cn } from '@/lib/utils';
import { useTRPC } from '@/lib/trpc/utils';
import { getMonthlyWelcomePromoMonth } from '@kilocode/web-shared/lib/kilo-pass/bonus';
import {
createKiloClawSignupDisplay,
formatMicrodollars,
Expand Down Expand Up @@ -92,11 +93,15 @@ function TierCard({
cadence,
isSelected,
onSelect,
showFirstMonthPromo,
subscriptionStartedAtIso,
}: {
tier: Tier;
cadence: Cadence;
isSelected: boolean;
onSelect: () => void;
showFirstMonthPromo: boolean;
subscriptionStartedAtIso: string;
}) {
const data = TIER_DATA[tier];
const isYearly = cadence === 'yearly';
Expand All @@ -105,6 +110,7 @@ function TierCard({
const creditsLabel = isYearly
? `$${data.yearlyPrice}/year paid credits`
: `$${data.monthlyPrice}/month paid credits`;
const promoMonth = getMonthlyWelcomePromoMonth(subscriptionStartedAtIso);

return (
<button
Expand All @@ -130,11 +136,19 @@ function TierCard({
<div className="text-muted-foreground text-xs leading-relaxed">
<span className="text-amber-300">{creditsLabel}</span>
</div>
<div className="text-muted-foreground text-xs leading-relaxed">
Up to <span className="text-emerald-300">40%</span> free bonus credits
</div>
{!isYearly && (
<div className="text-muted-foreground text-xs leading-relaxed">
Up to <span className="text-emerald-300">40%</span> free bonus credits
</div>
)}
<div className="text-xs leading-relaxed text-emerald-300">
First month: +50% free bonus credits
{isYearly
? '+50% free bonus credits every month'
: !showFirstMonthPromo
? 'Month 1: +5%; month 2: +10% free bonus credits'
: promoMonth === 1
? 'Month 1: +50%; month 2: +10% free bonus credits'
: 'Month 1: +5%; month 2: +50% free bonus credits'}
</div>
</div>

Expand Down Expand Up @@ -289,8 +303,17 @@ function HostingOnlyPlanCard({
);
}

function CreditsHowItWorks() {
function CreditsHowItWorks({
cadence,
showFirstMonthPromo,
subscriptionStartedAtIso,
}: {
cadence: Cadence;
showFirstMonthPromo: boolean;
subscriptionStartedAtIso: string;
}) {
const [open, setOpen] = useState(false);
const promoMonth = getMonthlyWelcomePromoMonth(subscriptionStartedAtIso);
return (
<div className="border-border/50 mb-3.5 overflow-hidden rounded-lg border">
<button
Expand Down Expand Up @@ -319,8 +342,26 @@ function CreditsHowItWorks() {
<div className="text-muted-foreground flex items-start gap-2 py-0.5 text-xs">
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0 text-emerald-400" />
<span>
First-time subscribers receive <span className="text-emerald-300">50%</span> free
bonus credits for the first month.
{cadence === 'yearly' ? (
<>Yearly subscribers receive 50% free bonus credits every month.</>
) : !showFirstMonthPromo ? (
<>
Monthly subscribers start with 5% free bonus credits, increasing by 5 percentage
points each consecutive month up to 40%.
</>
) : promoMonth === 1 ? (
<>
First-time monthly subscribers receive{' '}
<span className="text-emerald-300">50%</span> free bonus credits for the first
month, then the regular 10% bonus in the second month.
</>
) : (
<>
First-time monthly subscribers receive{' '}
<span className="text-emerald-300">50%</span> free bonus credits for the second
month, with the regular 5% bonus in the first month.
</>
)}
</span>
</div>
</div>
Expand Down Expand Up @@ -509,6 +550,9 @@ export function PlanSelectionDialog({ open, onOpenChange }: PlanSelectionDialogP
const { data: billing, isPending: billingSummaryPending } = useQuery(
trpc.kiloclaw.getPersonalBillingSummary.queryOptions()
);
const { data: kiloPassState } = useQuery(trpc.kiloPass.getState.queryOptions());
const showFirstMonthPromo = kiloPassState?.isEligibleForFirstMonthPromo === true;
const subscriptionStartedAtIso = new Date().toISOString();
const checkout = useMutation(trpc.kiloclaw.createSubscriptionCheckout.mutationOptions());
const kiloPassUpsell = useMutation(
trpc.kiloclaw.createKiloPassUpsellCheckout.mutationOptions({
Expand Down Expand Up @@ -729,13 +773,19 @@ export function PlanSelectionDialog({ open, onOpenChange }: PlanSelectionDialogP
key={tier}
tier={tier}
cadence={cadence}
showFirstMonthPromo={showFirstMonthPromo}
subscriptionStartedAtIso={subscriptionStartedAtIso}
isSelected={selectedTier === tier}
onSelect={() => handleTierSelect(tier)}
/>
))}
</div>

<CreditsHowItWorks />
<CreditsHowItWorks
cadence={cadence}
showFirstMonthPromo={showFirstMonthPromo}
subscriptionStartedAtIso={subscriptionStartedAtIso}
/>

{/* Warning */}
<div className="mb-4 flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2.5">
Expand Down
66 changes: 58 additions & 8 deletions apps/web/src/app/(app)/claw/components/billing/WelcomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Badge } from '@/components/ui/badge';
import { KiloPassIcon } from '@/components/icons/KiloPassIcon';
import { cn } from '@/lib/utils';
import { useTRPC } from '@/lib/trpc/utils';
import { getMonthlyWelcomePromoMonth } from '@kilocode/web-shared/lib/kilo-pass/bonus';
import {
createKiloClawSignupDisplay,
formatMicrodollars,
Expand Down Expand Up @@ -85,11 +86,15 @@ function TierCard({
cadence,
isSelected,
onSelect,
showFirstMonthPromo,
subscriptionStartedAtIso,
}: {
tier: Tier;
cadence: Cadence;
isSelected: boolean;
onSelect: () => void;
showFirstMonthPromo: boolean;
subscriptionStartedAtIso: string;
}) {
const data = TIER_DATA[tier];
const isYearly = cadence === 'yearly';
Expand All @@ -98,6 +103,7 @@ function TierCard({
const creditsLabel = isYearly
? `$${data.yearlyPrice}/year paid credits`
: `$${data.monthlyPrice}/month paid credits`;
const promoMonth = getMonthlyWelcomePromoMonth(subscriptionStartedAtIso);

return (
<button
Expand All @@ -123,11 +129,19 @@ function TierCard({
<div className="text-muted-foreground text-xs leading-relaxed">
<span className="text-amber-300">{creditsLabel}</span>
</div>
<div className="text-muted-foreground text-xs leading-relaxed">
Up to <span className="text-emerald-300">40%</span> free bonus credits
</div>
{!isYearly && (
<div className="text-muted-foreground text-xs leading-relaxed">
Up to <span className="text-emerald-300">40%</span> free bonus credits
</div>
)}
<div className="text-xs leading-relaxed text-emerald-300">
First month: +50% free bonus credits
{isYearly
? '+50% free bonus credits every month'
: !showFirstMonthPromo
? 'Month 1: +5%; month 2: +10% free bonus credits'
: promoMonth === 1
? 'Month 1: +50%; month 2: +10% free bonus credits'
: 'Month 1: +5%; month 2: +50% free bonus credits'}
</div>
</div>

Expand Down Expand Up @@ -282,8 +296,17 @@ function HostingOnlyPlanCard({
);
}

function CreditsHowItWorks() {
function CreditsHowItWorks({
cadence,
showFirstMonthPromo,
subscriptionStartedAtIso,
}: {
cadence: Cadence;
showFirstMonthPromo: boolean;
subscriptionStartedAtIso: string;
}) {
const [open, setOpen] = useState(false);
const promoMonth = getMonthlyWelcomePromoMonth(subscriptionStartedAtIso);
return (
<div className="border-border/50 mb-3.5 overflow-hidden rounded-lg border">
<button
Expand Down Expand Up @@ -312,8 +335,26 @@ function CreditsHowItWorks() {
<div className="text-muted-foreground flex items-start gap-2 py-0.5 text-xs">
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0 text-emerald-400" />
<span>
First-time subscribers receive <span className="text-emerald-300">50%</span> free
bonus credits for the first month.
{cadence === 'yearly' ? (
<>Yearly subscribers receive 50% free bonus credits every month.</>
) : !showFirstMonthPromo ? (
<>
Monthly subscribers start with 5% free bonus credits, increasing by 5 percentage
points each consecutive month up to 40%.
</>
) : promoMonth === 1 ? (
<>
First-time monthly subscribers receive{' '}
<span className="text-emerald-300">50%</span> free bonus credits for the first
month, then the regular 10% bonus in the second month.
</>
) : (
<>
First-time monthly subscribers receive{' '}
<span className="text-emerald-300">50%</span> free bonus credits for the second
month, with the regular 5% bonus in the first month.
</>
)}
</span>
</div>
</div>
Expand Down Expand Up @@ -502,6 +543,9 @@ export function WelcomePage() {
const { data: billing, isPending: billingSummaryPending } = useQuery(
trpc.kiloclaw.getPersonalBillingSummary.queryOptions()
);
const { data: kiloPassState } = useQuery(trpc.kiloPass.getState.queryOptions());
const showFirstMonthPromo = kiloPassState?.isEligibleForFirstMonthPromo === true;
const subscriptionStartedAtIso = new Date().toISOString();
const checkoutMutation = useMutation(trpc.kiloclaw.createSubscriptionCheckout.mutationOptions());
const kiloPassUpsell = useMutation(
trpc.kiloclaw.createKiloPassUpsellCheckout.mutationOptions({
Expand Down Expand Up @@ -720,13 +764,19 @@ export function WelcomePage() {
key={tier}
tier={tier}
cadence={cadence}
showFirstMonthPromo={showFirstMonthPromo}
subscriptionStartedAtIso={subscriptionStartedAtIso}
isSelected={selectedTier === tier}
onSelect={() => handleTierSelect(tier)}
/>
))}
</div>

<CreditsHowItWorks />
<CreditsHowItWorks
cadence={cadence}
showFirstMonthPromo={showFirstMonthPromo}
subscriptionStartedAtIso={subscriptionStartedAtIso}
/>

{/* Warning */}
<div className="mb-4 flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2.5">
Expand Down
Loading
Loading