हमने एक ऐसा प्लान बेचा जो काम नहीं करता था
हमने €4.90/महीना का प्लान लॉन्च किया। सालाना विकल्प हर महीने €58.80 काटता, प्लान कोई अधिकार नहीं देता था, और worker अपनी अवधि सीमा नज़रअंदाज़ कर रहा था। तीनों खुद खरीदकर पकड़े गए।
4 अक्टूबर 2026
बनाया गया: 4 अक्तूबर 2026 · अद्यतन: 4 अक्तूबर 2026
€4.90/महीना का एक प्लान हमारे प्राइसिंग पेज पर लाइव हो गया। जो कोई सालाना विकल्प चुनता, उससे हर महीने €58.80 कटते।
किसी से नहीं कटे।
हमने इसे तब पकड़ा जब एक भी ग्राहक ने सालाना प्लान नहीं खरीदा था — कोड पढ़कर नहीं, बल्कि असली कार्ड से खुद वह चीज़ खरीदकर।
यही बात अहम है। बिल्ड हरा था। TypeScript संतुष्ट था। API 200 लौटा रहा था। इनमें से किसी का मतलब यह नहीं था कि उत्पाद वाकई काम करता है।
उस सुबह हमें तीन खराबियाँ मिलीं। हमारा पहला भुगतान करने वाला Solo ग्राहक उन तीन सुधारों के आखिरी वाले के सत्ताईस मिनट बाद सब्सक्राइब हुआ। आधा घंटा दूसरी तरफ़ होता, और वही एक ऐसे प्लान के लिए पैसे दे रहा होता जो उसे कुछ नहीं देता।
खराबी 1: कीमत सही थी, अंतराल नहीं
हमारा नया एंट्री प्लान Solo सालाना बिलिंग पर €4.90/महीना है, या महीने-दर-महीने €8.90। €4.90 × 12 = €58.80 साल में एक बार।
सालाना विकल्प के पीछे वाली Stripe प्राइस मासिक बिलिंग अंतराल के साथ बनाई गई थी।
यानी Stripe हर महीने €58.80 — इच्छित कीमत का बारह गुना काटने के लिए तैयार था।
प्राइसिंग पेज सही था। कोड सही प्राइस आईडी इस्तेमाल कर रहा था। बग रिपॉज़िटरी के बाहर रहता था — डैशबोर्ड से बनाए गए एक Stripe ऑब्जेक्ट में।
इसी वजह से उसे चूकना आसान था।
Stripe किसी मौजूदा प्राइस का बिलिंग अंतराल बदलने नहीं देता: नई प्राइस बनाइए और आईडी बदल दीजिए। इसलिए हमने चेकआउट सेशन बनने से पहले एक जाँच जोड़ी:
const expected = effectivePeriod === "annual" ? "year" : "month";
const price = await stripe.prices.retrieve(priceId);
if (price.recurring?.interval !== expected) {
throw new Error("PRICE_INTERVAL_MISMATCH");
}
प्रति चेकआउट एक API कॉल।
अगर Stripe और प्राइसिंग पेज एक-दूसरे का खंडन करें, तो हम बेचने से इनकार कर देते हैं।
खराब Stripe ऑब्जेक्ट अब भी मौजूद रह सकता है। बस वह बिना ध्यान में आए चेकआउट से नहीं निकल सकता।
खराबी 2: प्लान व्यावसायिक रूप से था, कार्यात्मक रूप से नहीं
दूसरी खराबी उलटी दिशा में फेल हो रही थी: ग्राहक हमें भुगतान कर सकता था और फिर भी मुफ़्त जैसा बर्ताव पाता।
प्लान जोड़ना एक व्यावसायिक बदलाव जैसा लगता है। व्यवहार में यह उत्पाद की हर उस जगह को छूता है जो पूछती है: "इस अकाउंट को क्या करने की अनुमति है?"
हमारे कोडबेस में इस सवाल का जवाब 29 फ़ाइलों में दिया जा रहा था।
ये जाँचें समय के साथ एक्सपोर्ट, अपलोड, प्रोसेसिंग क़तार, API एंडपॉइंट, बिलिंग और डैशबोर्ड में जमा होती गई थीं। ज़्यादातर स्ट्रिंग तुलनाएँ थीं, जो उस समय मौजूद प्लानों के हिसाब से लिखी गई थीं जब वह कोड जोड़ा गया था।
Solo नया था।
इसलिए यह pro है, creator है या studio? जैसी जाँचें डिफ़ॉल्ट ब्रांच में गिर जाती थीं। और डिफ़ॉल्ट ब्रांच मुफ़्त वाली थी।
इसका मतलब था कि भुगतान करने वाला Solo अकाउंट मुफ़्त कोटा, मुफ़्त अपलोड सीमाएँ, वॉटरमार्क और मुफ़्त स्तर की बाकी पाबंदियाँ पा सकता था।
उस डिफ़ॉल्ट के पीछे की सहज बुद्धि बेवकूफ़ी नहीं थी। सुरक्षा में "fail closed" आमतौर पर वही है जो आप चाहते हैं।
लेकिन बिलिंग में यह एक अलग तरह की विफलता बनाता है: ग्राहक ने पहले ही भुगतान कर दिया है, और आपका सबसे सुरक्षित सहारा ठीक वही चीज़ बन जाता है जो उससे उसका खरीदा हुआ रोक लेती है।
सुधार यह नहीं था कि "solo" को 29 शर्तों में जोड़ दें। हमने उसकी जगह एक ही जगह बनाई जहाँ प्लान का व्यवहार वर्णित होता है:
export type Plan = "free" | "solo" | "pro" | "creator" | "studio";
export function isPaid(plan): boolean
export function hasDubbing(plan): boolean
export function hasScheduling(plan): boolean
export function videoQuota(plan, status): number
export function maxDurationMinutes(plan): number
export function maxUploadSize(plan): number
export function retentionHours(plan): number | null
ऊपर के नाम लेख के लिए सरल किए गए हैं, पर ढाँचा वही है जो हमने तैनात किया।
उस फ़ाइल के शीर्ष पर अब नियम लिखा है: जब तक कोई प्लान यहाँ नहीं है, वह मौजूद नहीं है।
सुधार के दौरान दो सीखने लायक नज़दीकी चूकें हुईं।
पहली, हमने लगभग उस एक isPaid() जाँच को हर गेट के लिए इस्तेमाल कर लिया था, उन गेटों के लिए भी जो किसी ख़ास फ़ीचर के हैं। इससे Solo ग्राहकों को वे चीज़ें मिल जातीं जिनके वे पैसे नहीं देते — जैसे AI डबिंग और पोस्ट शेड्यूलिंग। "भुगतान करने वाला" और "इस फ़ीचर का अधिकारी" एक ही सवाल नहीं हैं।
दूसरी, हमें कुछ देर लगा कि शेड्यूलिंग में कोई प्लान जाँच ही नहीं है, क्योंकि रूट में "plan" खोजने पर कुछ नहीं मिला। गेट एक साझा हेल्पर के भीतर था, एक कॉल की दूरी पर।
खाली लौटा grep कोई जवाब नहीं है।
खराबी 3: वह सीमा जिसका एलान किया पर लागू कभी नहीं
प्राइसिंग पेज कहता है कि Solo 60 मिनट तक के स्रोत वीडियो स्वीकार करता है। जो worker वीडियो डाउनलोड और प्रोसेस करता है, वह केवल दो सीमाएँ जानता था:
- मुफ़्त अकाउंट के लिए 30 मिनट
- भुगतान वाले अकाउंट के लिए 90 मिनट
Solo भुगतान वाला है। इसलिए Solo को 90 मिला।
यह खराबी उलटी दिशा में रिस रही थी। किसी से ज़्यादा नहीं वसूला गया और किसी की पहुँच नहीं गई। हम बस प्लान में शामिल होने चाहिए उससे ज़्यादा कंप्यूट मुफ़्त दे रहे थे।
वीडियो उत्पाद के लिए यह मायने रखता है। स्रोत की अवधि उन कारकों में से है जो ट्रांसक्रिप्शन, रेंडरिंग और बैंडविड्थ की लागत से सबसे सीधे जुड़े हैं।
worker अब वही ऊपरी सीमा रखता है जो ऐप्लिकेशन रखती है:
def _max_minutes(plan: str | None) -> int:
"""Mirror of the plan limits used by the app.
What the pricing page announces and what the worker enforces
must be the same number.
A cap announced without being applied is margin walking out;
applied without being announced is a trap.
"""
जो सीमा केवल प्राइसिंग पेज पर मौजूद है, वह सीमा नहीं है। वह एक पेज पर लिखा एक वाक्य है।
इन बग्स को असल में किसने पकड़ा
टाइप चेकर ने नहीं। टेस्ट सूट ने नहीं। कोड रिव्यू ने नहीं।
इन्हें पकड़ा साइट खोलकर, यह दिखावा करके कि हमने उसे कभी देखा ही नहीं, सबसे सस्ता विकल्प क्लिक करके और असली कार्ड डालकर।
बिलिंग की गड़बड़ Stripe के अपने चेकआउट सारांश में दिखी। गायब अधिकार तब दिखे जब भुगतान वाला अकाउंट मुफ़्त जैसा ही बर्ताव करता रहा। अवधि की समस्या तब दिखी जब हमने worker से पूछा कि वह असल में कौन सी सीमा लगाएगा।
हम वही सबक बार-बार सीखते हैं: पास होता बिल्ड यह नहीं साबित करता कि उत्पाद काम करता है।
हर बग दो ऐसी प्रणालियों के बीच की दरार में रहता था जो अलग-अलग ठीक वही कर रही थीं जो उन्हें कहा गया था:
- प्राइसिंग पेज और Stripe
- प्लान का नाम और उसके असली अधिकार
- ऐप्लिकेशन और worker
इस किस्म के बग ढूँढ़ने का सस्ता तरीका हैरान करने वाला उबाऊ है: उत्पाद को वैसे इस्तेमाल कीजिए जैसे उसके लिए पैसे देने वाला इस्तेमाल करेगा।
एट्रिब्यूशन पर एक उपसंहार
हमारा पहला Solo सब्सक्राइबर उसी सुबह आया।
करीब एक घंटे तक हमने खुद को एक सुंदर कहानी सुनाई: Solo अभी लॉन्च हुआ, किसी ने उसे खुद ढूँढ़ा, मुफ़्त सीमा से टकराया और कन्वर्ट हो गया।
इवेंट लॉग ने कुछ कम सुव्यवस्थित कहानी बताई।
उस व्यक्ति ने Katto को ChatGPT के ज़रिए पाया, साइन अप किया, मुफ़्त प्लान का भरपूर इस्तेमाल किया, कोटा ख़त्म किया, कई बार चेकआउट खोला और बिना भुगतान चला गया।
अगली सुबह छोड़े गए चेकआउट का एक ईमेल गया। कुछ घंटों बाद उन्होंने सब्सक्राइब किया।
उन्होंने सालाना बिलिंग वाले €4.90 के बजाय महीने-दर-महीने €8.90 चुना — बारह महीने की प्रतिबद्धता से बचने के लिए प्रति महीना 82% अधिक देकर। एक दिन पहले जिस फ़ाउंडिंग ऑफ़र पर उन्होंने क्लिक किया और छोड़ा था, वह €6 का था।
हमने यह भी मान लिया था कि स्रोत वीडियो की 60 मिनट की सीमा ने उन्हें Solo की ओर धकेला। ऐसा नहीं था। वे बार-बार महीने में दो वीडियो के कोटे से टकरा रहे थे।
तो दो धारणाएँ एक साथ मरीं। कन्वर्ज़न बस ऑर्गेनिक नहीं था — रिकवरी ईमेल का असर संभवतः था। और अपग्रेड का सबसे ज़्यादा दबाव बनाने वाली सीमा वह नहीं थी जो हम समझ रहे थे।
इसका आकार ऊपर की तीन खराबियों जैसा ही है। बाहर से एक साफ़ कहानी कहना आसान था। फिर हमने देखा कि असल में क्या हुआ।
Katto लंबे वीडियो को वर्टिकल क्लिप में बदलता है। हम वही आँकड़े प्रकाशित करते हैं जो हम मापते हैं — वे भी जिनसे हम बुरे दिखते हैं।
संबंधित लेख
अपने वीडियो को वायरल क्लिप में बदलने के लिए तैयार हैं?
Katto आपके लंबे वीडियो को अपने आप काटता, कैप्शन जोड़ता और रीफ़्रेम करके शॉर्ट-फ़ॉर्म कंटेंट बनाता है।
Katto मुफ़्त आज़माएँ →