In the first 24 hours after launching Learnflow AI, I was quietly optimistic. Users signed up. Sessions were started. Feedback was decent.
But something felt off.
No one upgraded.
Despite building a clean onboarding flow, embedding Kinde's hosted pricing tables, and giving users 10 free credits to try the platform, conversion sat at 0%.
I didn’t just need a payment integration. I needed a pricing model that matched user behavior — and a credit system that made value feel tangible.
This post is about the system I built, broke, rebuilt, and finally shipped to convert confused trial users into paying customers. It's also about how I used Convex, Kinde, and Vapi to make it all happen.
The Problem With Invisible Usage
When I launched Learnflow AI, I decided to make usage credit-based: 1 session = 1 credit. Every user got 10 free credits. Pro plans included 100/month.
It sounded clean. Scalable. Friendly.
But my first users weren’t behaving like they understood any of it.
Here's what I observed:
- Users would start a session, then bounce.
- Some never used more than 1 credit.
- A few returned days later and seemed surprised that their credits were gone (initially, I made it available for 14 days to ramp up usage).
The credit system existed. It was tracked. But it wasn’t felt.
That’s when I realized:
Abstract usage doesn’t drive upgrades. Visible, valuable usage does.
Step 1: Define Usage as a Business Layer
I built Learnflow AI in a single weekend using:
- Convex for backend + real-time reactivity
- Kinde for auth + billing logic
- Vapi to abstract the entire voice session loop
Credits became the first real business abstraction I needed to enforce.
Convex Schema:
users: defineTable({
email: v.string(),
plan: v.optional(v.string()), // 'free' or 'pro'
credits: v.optional(v.number()),
});
Every time a session started, I ran a mutation:
const creditCost = user.plan === 'pro' ? 0 : 1;
if (user.credits < creditCost) {
throw new Error('Out of credits. Please upgrade.');
}
await db.patch(user._id, {
credits: user.credits - creditCost,
});
This enforced usage. But it still didn’t communicate usage.
Step 2: Surface Usage in the UI
The first iteration of Learnflow showed no credit balance. Just a dashboard of public tutors and a "Start Session" button.
Users were flying blind.
So I shipped three changes:
1. Sticky Header Counter
<Badge>Credits Left: {user.credits}</Badge>
This sat in the global header. No tabs. No modals. It was always visible.
2. Session Recap Modal
After each session:
<p>"You've used 1 credit. You have {user.credits} remaining."</p>
3. Tutor Card Labels
<Label>{plan === 'free' ? 'Free Access' : 'Pro Feature'}</Label>
Each of these made the system tangible. Users began associating credits with action.
Step 3: Build Pricing Into the Journey
Kinde handles pricing at the account level. When users signed up, they hit a hosted pricing table.
Flow:
User signs up
→ Lands on Kinde-hosted pricing table
→ Picks plan (Free or Pro)
→ Metadata stored on user
→ Redirect to Learnflow dashboard
I extracted plan and credit info from Kinde like this:
const { profile } = useUserContext();
const { isAuthenticated, getAccessTokenRaw } = useKindeAuth();
const [entitlements, setEntitlements] = useState<EntitlementsData | null>(null);
if (!isAuthenticated) {
return <div>You are not logged in, please log in.</div>;
}
useEffect(() => {
const fetchEntitlements = async () => {
const accessToken = await getAccessTokenRaw();
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_KINDE_ISSUER_URL}/account_api/v1/entitlements`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
cache: "no-store",
});
const data = await res.json();
console.log("Entitlements payload:", data);
setEntitlements(data as EntitlementsData);
} catch (error) {
console.error("Error fetching entitlements:", error);
}
};
fetchEntitlements();
}, [getAccessTokenRaw]);
let plan: "free" | "pro" = "free";
const plans = entitlements?.data?.plans ?? [];
console.log("Plans:", plans);
if (plans.some((p: any) => p.key === "pro")) {
plan = "pro";
} else if (plans.some((p: any) => p.key === "free")) {
plan = "free";
}
console.log("Plan:", plan);
I used this plan field to gate features on the frontend and inside Convex mutations.
But here's the catch:
Once the user is inside the product, they rarely revisit the pricing table on the billing page.
So I brought pricing to them.
Key UX Prompts:
- If user is out of credits: full-screen modal with "Upgrade to Pro"
- If user clicks a Pro-only tutor: inline modal explaining Pro perks
- If user is on their last credit: banner warning them
Flow Diagram: Usage + Upgrade System
Step 4: Handle Edge Cases
A few tricky cases came up once people started returning:
Switching Devices
If a user upgraded on one device but their session still had stale data (cached plan or credits), they'd get blocked incorrectly.
Fix:
- Fetched fresh credit/plan info on each app load
- Added loading gate to dashboard to ensure correct plan before any session began
Retry After Upgrade
Some users upgraded after hitting 0, then retried but still saw the error modal.
Fix:
- On upgrade, re-pulled session from Kinde via a backend sync
- Refreshed local state using
useEffect
hook with plan/credit dependency
What Changed After Shipping
Once I made credit usage visible and value obvious, behavior shifted.
Numbers (first 30 users):
- 70% used at least 5 credits
- Around 30% upgraded to Pro
- Zero support tickets asking "What are credits?"
I didn’t add more features.
I made the business logic clearer.
What I Learned
- Credits must feel like currency. They’re not abstract limits, they’re fuel to your product.
- Upgrades must be contextual. Don’t send users to a billing page. Put the prompt where the pain is.
- Metadata isn’t enough. Storing plan = "pro" is easy. Reflecting that in the experience is the work.
A Note on Kinde
Kinde did exactly what I needed:
- Hosted signup and pricing table
- Stored plan + metadata per user
- Role-based gating support in frontend logic
The hosted pricing table alone saved me days.
But the biggest value wasn’t features — it was clarity.
Once pricing and access were defined, the rest was just logic.
Final Thoughts
If you're building an AI product — especially one with per-session cost — you can't afford ambiguous usage.
You need clarity:
- When does usage happen?
- What do users get?
- What happens when it runs out?
Your credit system isn't just backend logic.
It's how you translate value.
Your Turn
If you're launching a usage-based tool:
- What’s your model?
- What’s your feedback loop?
- Where does pricing show up?
Let’s trade notes in the comments.
PS: Want to try Learnflow AI? Drop a comment also, I’ll send you a link to get early access.
Top comments (2)
definitely something i can learn from, thanks for sharing
You are welcome, appreciate you taking the time to read it.