Skip to content

Commit a445458

Browse files
markijbemaRSO
andauthored
feat(kilo-pass): guard welcome promo by payment fingerprint (#3526)
* docs(kilo-pass): plan card-based welcome promo guard * docs(referrals): define Kilo Pass card reuse eligibility * feat(kilo-pass): persist card promo eligibility claims * feat(kilo-pass): gate welcome promo by settled card * feat(kilo-pass): warn when welcome promo is unavailable * fix(kilo-pass): scope promo warning to checkout session * test(gdpr): retain Kilo Pass card claim evidence * refactor(kilo-pass): derive promo eligibility from reason * docs(kilo-pass): broaden promo guard to payment fingerprints * fix(kilo-pass): enforce settled payment fingerprint guard * fix(kilo-pass): require checkout session for promo warning * fix(kilo-pass): require affirmative Stripe promo decision --------- Co-authored-by: Remon Oldenbeuving <remon@kilocode.ai>
1 parent 5b1108a commit a445458

21 files changed

Lines changed: 25588 additions & 18 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ yarn-error.log*
6767
/.plans/*
6868
!/.plans/experimental-models-1.md
6969
!/.plans/experimental-models-2.md
70+
!/.plans/kilo-pass-welcome-promo-card-fingerprint-guard.md
7071
/.plan/
7172
.kilo/plans
7273
.superpowers/

.plans/kilo-pass-welcome-promo-card-fingerprint-guard.md

Lines changed: 173 additions & 0 deletions
Large diffs are not rendered by default.

.specs/impact-referrals.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,45 @@ conversion, local referral rewards are authoritative and affiliate SALE reportin
689689
175. Before launch, the existing internal referral system MUST be scoped away from KiloClaw and Kilo Pass, disabled for
690690
those products, or migrated into this program's rules to prevent double rewards.
691691

692+
### Kilo Pass Reusable Payment-Fingerprint Welcome-Promo Guard
693+
694+
176. The Kilo Pass introductory welcome promo MUST be claimable at most once per reusable Stripe payment-instrument
695+
fingerprint that the system supports. Initial supported instrument types are `card`, `sepa_debit`,
696+
`us_bank_account`, `bacs_debit`, and `au_becs_debit`. Annual subscriptions are excluded.
697+
698+
177. An instrument fingerprint opportunity MUST be claimed only when a personal monthly Kilo Pass Stripe payment with
699+
`amount_paid > 0` settles using that instrument, not when the instrument is merely attached, when a zero-value
700+
invoice is finalized, when usage crosses the bonus threshold, or when welcome-promo credits are later issued.
701+
702+
178. A first-time account whose positively paid monthly Kilo Pass settlement uses a previously claimed reusable instrument
703+
fingerprint MUST NOT receive the introductory `50%` welcome promo. It MAY receive the ordinary monthly-ramp bonus
704+
using the same behavior as an existing or previously canceled Kilo Pass subscriber.
705+
706+
179. A positively paid monthly Kilo Pass settlement using a previously claimed reusable instrument fingerprint MUST NOT be
707+
an eligible Kilo Pass referral conversion and MUST NOT grant a Kilo Pass referral reward to either beneficiary role.
708+
709+
180. A positively paid monthly settlement whose payment method is confirmed not to provide a supported reusable
710+
fingerprint MUST NOT be disqualified solely because no cross-account instrument signal exists. A supported reusable
711+
method with an absent fingerprint MAY follow that fallback. An unresolvable settlement MUST NOT be treated as a
712+
confirmed eligible fallback.
713+
714+
181. Shared household, business, or other jointly used payment instruments are governed by the same one-claim rule; the
715+
system MUST NOT provide additional welcome-promo or Kilo Pass referral-conversion eligibility solely because a later
716+
buyer is a different person.
717+
718+
182. A claimed reusable instrument fingerprint MUST remain claimed after refund, dispute, fraud marking, cancellation,
719+
failure to redeem welcome-promo credits, or account deletion. Credit or reward handling for adverse payments is
720+
separate from instrument-claim retention.
721+
722+
183. When a paid monthly purchase is welcome-promo ineligible because its reusable payment fingerprint was previously
723+
claimed, the post-payment Kilo Pass confirmation flow MUST inform the customer that the introductory bonus does not
724+
apply. The message MUST NOT expose the fingerprint or the existence or identity of another account.
725+
726+
184. Durable instrument-claim records MAY retain the minimum Stripe fingerprint and payment identity data needed to
727+
enforce these anti-abuse rules after account deletion. Customer-facing surfaces MUST NOT expose that retained
728+
evidence, and any direct user identity references stored with such records MUST be deleted or anonymized under GDPR
729+
deletion flows.
730+
692731
## Error Handling
693732

694733
1. If referral touch capture fails, the system SHOULD log the failure and continue the primary request.
@@ -727,6 +766,13 @@ conversion, local referral rewards are authoritative and affiliate SALE reportin
727766

728767
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.
729768

769+
### 2026-05-27 -- Prevent repeated Kilo Pass welcome claims by payment fingerprint
770+
771+
Added the Kilo Pass reusable Stripe payment-fingerprint guard for monthly introductory welcome promos and referral
772+
conversions. The first positively paid settlement using a supported fingerprintable instrument permanently claims that
773+
instrument opportunity; reused instruments retain ordinary monthly-ramp bonus behavior but do not receive the
774+
introductory promo or create Kilo Pass referral rewards. Annual behavior remains outside this restriction.
775+
730776
### 2026-05-22 -- Rename and expand to Kilo Pass
731777

732778
Renamed `.specs/kiloclaw-referrals.md` to `.specs/impact-referrals.md`. Generalized shared Impact Advocate referral

apps/web/src/app/payments/kilo-pass/awarding/KiloPassAwardingCreditsClient.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
77

88
import BigLoader from '@/components/BigLoader';
99
import { PageContainer } from '@/components/layouts/PageContainer';
10+
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
1011
import { Button } from '@/components/ui/button';
1112
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
1213
import { useTRPC } from '@/lib/trpc/utils';
@@ -37,13 +38,27 @@ function stepStatus(step: ActivationStep, current: ActivationStep): 'done' | 'ac
3738
return 'pending';
3839
}
3940

41+
function WelcomePromoIneligibleNotice() {
42+
return (
43+
<Alert variant="warning">
44+
<AlertTriangle />
45+
<AlertTitle>Introductory bonus not available</AlertTitle>
46+
<AlertDescription>
47+
This payment method has already been used for the introductory Kilo Pass bonus. Your
48+
subscription remains active and standard monthly bonus terms apply.
49+
</AlertDescription>
50+
</Alert>
51+
);
52+
}
53+
4054
export function KiloPassAwardingCreditsClient() {
4155
const trpc = useTRPC();
4256
const router = useRouter();
4357
const searchParams = useSearchParams();
4458
const [didTimeout, setDidTimeout] = useState(false);
4559
const [redirectSecondsRemaining, setRedirectSecondsRemaining] = useState<number | null>(null);
4660

61+
const checkoutSessionId = searchParams.get('session_id') ?? '';
4762
const clawHostingPlan = searchParams.get('clawHostingPlan');
4863
const clawInstanceId = searchParams.get('clawInstanceId');
4964
const isClawAutoActivation = !!clawHostingPlan;
@@ -76,7 +91,8 @@ export function KiloPassAwardingCreditsClient() {
7691
}, []);
7792

7893
const query = useQuery({
79-
...trpc.kiloPass.getCheckoutReturnState.queryOptions(),
94+
...trpc.kiloPass.getCheckoutReturnState.queryOptions({ sessionId: checkoutSessionId }),
95+
enabled: checkoutSessionId.length > 0,
8096
refetchInterval: query => {
8197
const data = query.state.data;
8298
if (didTimeout) return false;
@@ -89,6 +105,8 @@ export function KiloPassAwardingCreditsClient() {
89105

90106
const isReady = query.data?.creditsAwarded === true;
91107
const hasSubscription = query.data?.subscription != null;
108+
const showWelcomePromoIneligibleNotice =
109+
query.data?.welcomePromoIneligibleDueToReusedFingerprint === true;
92110

93111
// For KiloClaw auto-activation: advance step when credits are awarded, then enroll
94112
useEffect(() => {
@@ -276,6 +294,7 @@ export function KiloPassAwardingCreditsClient() {
276294
<div className="text-muted-foreground text-sm">
277295
You can activate hosting manually from the KiloClaw dashboard.
278296
</div>
297+
{showWelcomePromoIneligibleNotice ? <WelcomePromoIneligibleNotice /> : null}
279298
<div className="flex flex-wrap gap-2">
280299
<Button type="button" onClick={() => router.replace('/claw')}>
281300
Go to KiloClaw
@@ -303,6 +322,7 @@ export function KiloPassAwardingCreditsClient() {
303322
</CardHeader>
304323
<CardContent className="grid gap-4">
305324
<ActivationSteps current="done" />
325+
{showWelcomePromoIneligibleNotice ? <WelcomePromoIneligibleNotice /> : null}
306326
<div className="flex flex-wrap items-center gap-3">
307327
<Button type="button" onClick={() => router.replace('/claw')}>
308328
Continue to KiloClaw
@@ -357,6 +377,8 @@ export function KiloPassAwardingCreditsClient() {
357377
Your Kilo Pass is active and your credits are ready.
358378
</div>
359379

380+
{showWelcomePromoIneligibleNotice ? <WelcomePromoIneligibleNotice /> : null}
381+
360382
<div className="flex flex-wrap items-center gap-3">
361383
<Button type="button" onClick={() => router.replace('/profile')}>
362384
Continue to profile

apps/web/src/lib/kilo-pass/enums.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export {
44
KiloPassPaymentProvider,
55
KiloPassIssuanceSource,
66
KiloPassIssuanceItemKind,
7+
KiloPassWelcomePromoPaymentFingerprintType,
8+
KiloPassWelcomePromoEligibilityReason,
79
KiloPassAuditLogAction,
810
KiloPassAuditLogResult,
911
KiloPassScheduledChangeStatus,

0 commit comments

Comments
 (0)