Skip to content
Open
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
43 changes: 34 additions & 9 deletions .specs/impact-referrals.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Updated 2026-05-06 -- require Impact Advocate reward redemption after local Kilo
Updated 2026-05-12 -- note price-versioned KiloClaw billing preserves referral semantics.
Updated 2026-05-22 -- renamed to `.specs/impact-referrals.md` and expanded to Kilo Pass referrals.
Updated 2026-05-28 -- classify enforced Stripe EFW refunds as adverse payments.
Updated 2026-05-29 -- name the Impact-facing Kilo Pass reward unit `Kilo Pass Bonus Credits`.

## Conventions

Expand Down Expand Up @@ -120,9 +121,6 @@ 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.

## Overview

Expand All @@ -143,6 +141,8 @@ not stack with, the normal Kilo Pass monthly/promo bonus for that issuance.
Existing Impact Performance conversion events drive Impact Advocate conversion state. The system uses `Sale (71659)` as
the paid-conversion event for referral conversion and renewal reporting. When referral wins attribution for a paid
conversion, local referral rewards are authoritative and affiliate SALE reporting for the same conversion is suppressed.
Impact Advocate reward redemption is used only for reporting synchronization: KiloClaw redeems after local free-month
application, and Kilo Pass redeems after local referral bonus allocation.

## Rules

Expand All @@ -157,8 +157,9 @@ conversion, local referral rewards are authoritative and affiliate SALE reportin
- UTT UUID: `A7138521-9724-4b8f-95f4-1db2fbae81141`
- Advocate widget ID: `p/51699/w/referrerWidget`

3. Existing unscoped Impact Advocate configuration MAY remain as KiloClaw fallback configuration only. Kilo Pass MUST
require explicit Kilo Pass Advocate program/widget configuration and MUST NOT fall back to KiloClaw configuration.
3. Impact Advocate account SID, auth token, and tenant alias MAY be shared across Advocate programs. KiloClaw and Kilo
Pass MUST each require explicit product-scoped Advocate program ID and widget ID configuration. Products MUST NOT
fall back to unscoped or other-product program/widget configuration.

4. Kilo Pass MUST use a different Impact Advocate program ID and widget ID than KiloClaw.

Expand Down Expand Up @@ -543,9 +544,9 @@ conversion, local referral rewards are authoritative and affiliate SALE reportin
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. Kilo Pass welcome bonus behavior MUST grant the 50% promo only for the first monthly streak month of a
first-time subscriber. Streak month 2 and later MUST use the normal monthly ramp unless a referral bonus replaces
the bonus for that issuance.

### Shared Reward Granting

Expand Down Expand Up @@ -627,6 +628,16 @@ conversion, local referral rewards are authoritative and affiliate SALE reportin
158. Impact reward redemption state is for reporting and reconciliation only. It MUST NOT be the source of truth for
local reward eligibility, application, cancellation, or reversal.

158a. For Kilo Pass, when a local referral bonus reward is allocated/granted, the system MUST queue asynchronous Impact
Advocate reward lookup and single-reward redemption using the USD-denominated reward amount and the
`Kilo Pass Bonus Credits` unit so Impact reporting matches Kilo allocation state.

158b. Kilo Pass Impact Advocate reward redemption MUST be idempotently queued per local reward and MUST NOT block paid
conversion processing, reward ledger creation, reward application, billing settlement, or user access.

158c. Kilo Pass Impact reward lookup and redemption state is for reporting and reconciliation only. It MUST NOT be the
source of truth for local reward eligibility, application, cancellation, or reversal.

### Refunds, Reversals, and Fraud

159. Rewards from a qualifying Stripe payment MUST be treated as adverse when Stripe reports a chargeback or when
Expand Down Expand Up @@ -762,6 +773,10 @@ conversion, local referral rewards are authoritative and affiliate SALE reportin

## Changelog

### 2026-05-29 -- Name the Kilo Pass Impact reward unit

Kilo Pass reward synchronization sends the `Kilo Pass Bonus Credits` unit to Impact Advocate while retaining the USD-denominated local reward amount.

### 2026-05-28 -- Enforced EFW refunds are adverse payments

Classified an enforced Stripe Early Fraud Warning refund as an adverse qualifying payment for both covered products. Pending or earned-but-unapplied rewards cancel, already-applied rewards require support review, and later refund or chargeback delivery must remain idempotent.
Expand All @@ -773,14 +788,24 @@ conversions. The first positively paid settlement using a supported fingerprinta
instrument opportunity; reused instruments retain ordinary monthly-ramp bonus behavior but do not receive the
introductory promo or create Kilo Pass referral rewards. Annual behavior remains outside this restriction.

### 2026-05-26 -- Redeem allocated Kilo Pass rewards in Impact Advocate

Kilo Pass referral bonus allocation now queues Impact Advocate reward lookup and redemption using the USD reward amount,
for reporting synchronization only. The local reward ledger remains authoritative.

### 2026-05-25 -- Require product-scoped Advocate program/widget configuration

Removed KiloClaw fallback to unscoped Impact Advocate program/widget configuration. KiloClaw and Kilo Pass now both
require explicit product-scoped Advocate program ID and widget ID while sharing account SID, auth token, and tenant alias.

### 2026-05-22 -- Rename and expand to Kilo Pass

Renamed `.specs/kiloclaw-referrals.md` to `.specs/impact-referrals.md`. Generalized shared Impact Advocate referral
rules and added Kilo Pass referral requirements: separate program/widget config, `/subscriptions/kilo-pass/refer`,
monthly Stripe launch scope, referral-vs-affiliate priority using the KiloClaw resolver model, first-time Kilo Pass
subscriber eligibility, 5-referrer-reward cap, double-sided 50% rewards snapshotted from the referee's monthly tier,
12-month pending reward expiry, base-issuance `referral_bonus` fulfillment, monthly bonus replacement, adverse-payment
handling, server-side Performance SALE reporting for Advocate state, and welcome-bonus cutoff behavior.
handling, server-side Performance SALE reporting for Advocate state, and first-month welcome-bonus behavior.

### 2026-05-12 -- Price-versioned KiloClaw billing preserves referral semantics

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ import {
type KiloClawSignupDisplay,
type KiloPassUpsellActivationPreview,
} from './billing-types';
import { KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF } from '@/lib/kilo-pass/constants';
import { dayjs } from '@/lib/kilo-pass/dayjs';

type Cadence = 'monthly' | 'yearly';
type Tier = '19' | '49' | '199';

Expand Down Expand Up @@ -137,7 +134,7 @@ function TierCard({
Up to <span className="text-emerald-300">40%</span> free bonus credits
</div>
<div className="text-xs leading-relaxed text-emerald-300">
First 2 months: +50% free bonus credits
First month: +50% free bonus credits
</div>
</div>

Expand Down Expand Up @@ -293,8 +290,6 @@ function HostingOnlyPlanCard({

function CreditsHowItWorks() {
const [open, setOpen] = useState(false);
const showTwoMonthPromo = dayjs().utc().isBefore(KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF);

return (
<div className="border-border/50 mb-3.5 overflow-hidden rounded-lg border">
<button
Expand All @@ -320,15 +315,13 @@ function CreditsHowItWorks() {
expire monthly.
</span>
</div>
{showTwoMonthPromo ? (
<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 two months.
</span>
</div>
) : null}
<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.
</span>
</div>
</div>
)}
</div>
Expand Down Expand Up @@ -519,7 +512,7 @@ export function PlanSelectionDialog({ open, onOpenChange }: PlanSelectionDialogP
const kiloPassUpsell = useMutation(
trpc.kiloclaw.createKiloPassUpsellCheckout.mutationOptions({
onSuccess: data => {
if (data.url) window.location.href = data.url;
if (data.url) window.location.assign(data.url);
},
})
);
Expand Down
25 changes: 9 additions & 16 deletions apps/web/src/app/(app)/claw/components/billing/WelcomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ import {
type KiloClawSignupDisplay,
type KiloPassUpsellActivationPreview,
} from './billing-types';
import { KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF } from '@/lib/kilo-pass/constants';
import { dayjs } from '@/lib/kilo-pass/dayjs';

type Cadence = 'monthly' | 'yearly';
type Tier = '19' | '49' | '199';

Expand Down Expand Up @@ -130,7 +127,7 @@ function TierCard({
Up to <span className="text-emerald-300">40%</span> free bonus credits
</div>
<div className="text-xs leading-relaxed text-emerald-300">
First 2 months: +50% free bonus credits
First month: +50% free bonus credits
</div>
</div>

Expand Down Expand Up @@ -286,8 +283,6 @@ function HostingOnlyPlanCard({

function CreditsHowItWorks() {
const [open, setOpen] = useState(false);
const showTwoMonthPromo = dayjs().utc().isBefore(KILO_PASS_MONTHLY_FIRST_2_MONTHS_PROMO_CUTOFF);

return (
<div className="border-border/50 mb-3.5 overflow-hidden rounded-lg border">
<button
Expand All @@ -313,15 +308,13 @@ function CreditsHowItWorks() {
expire monthly.
</span>
</div>
{showTwoMonthPromo ? (
<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 two months.
</span>
</div>
) : null}
<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.
</span>
</div>
</div>
)}
</div>
Expand Down Expand Up @@ -512,7 +505,7 @@ export function WelcomePage() {
const kiloPassUpsell = useMutation(
trpc.kiloclaw.createKiloPassUpsellCheckout.mutationOptions({
onSuccess: data => {
if (data.url) window.location.href = data.url;
if (data.url) window.location.assign(data.url);
},
})
);
Expand Down
16 changes: 15 additions & 1 deletion apps/web/src/app/(app)/subscriptions/kilo-pass/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import { redirect } from 'next/navigation';

import { PageContainer } from '@/components/layouts/PageContainer';
import { KiloPassDetail } from '@/components/subscriptions/kilo-pass/KiloPassDetail';
import { db } from '@/lib/drizzle';
import { getKiloPassStateForUser } from '@/lib/kilo-pass/state';
import { getUserFromAuthOrRedirect } from '@/lib/user/server';

export default async function KiloPassSubscriptionPage() {
const user = await getUserFromAuthOrRedirect(
'/users/sign_in?callbackPath=/subscriptions/kilo-pass'
);
const subscription = await getKiloPassStateForUser(db, user.id);

if (!subscription) {
redirect('/subscriptions');
}

export default function KiloPassSubscriptionPage() {
return (
<PageContainer>
<KiloPassDetail />
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/app/(app)/subscriptions/kilo-pass/refer/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import { useQuery } from '@tanstack/react-query';

import { ImpactAdvocateReferralWidget } from '@/components/referrals/ImpactAdvocateReferralCard';
import { KiloPassReferralPageContent } from '@/components/referrals/KiloPassReferralPageContent';
import { useTRPC } from '@/lib/trpc/utils';

export default function KiloPassReferralPage() {
const trpc = useTRPC();
const rewardSummary = useQuery(trpc.kiloPass.getReferralRewardSummary.queryOptions());

return (
<KiloPassReferralPageContent
summary={rewardSummary.data ?? null}
isLoading={rewardSummary.isLoading}
errorMessage={
rewardSummary.isError ? 'Rewards are temporarily unavailable. Try again in a minute.' : null
}
>
<ImpactAdvocateReferralWidget product="kilo_pass" />
</KiloPassReferralPageContent>
);
}
Loading
Loading