DEV Community

Tahseen Rahman
Tahseen Rahman

Posted on

How I Built a Credit Card Rewards Optimizer with React Native, Supabase, and Stripe in 2 Weeks

Two weeks. One idea. 410 credit cards. A working subscription app with AI built in.

This is the technical story of how I built Rewardly — a credit card rewards optimizer for Canadians — from zero to production, solo, in 14 days.

The Problem I Was Solving

Canadians have some of the best credit card rewards programs in the world. But the comparison tools are terrible. Most sites are US-first, affiliate-stuffed, and tell you nothing useful about your specific spending patterns.

The real question isn't "what's the best credit card?" It's: given how I actually spend money, which card — or combination of cards — maximizes my rewards?

That's a data problem. I knew I could build something better.

Tech Stack (Made in 10 Minutes)

  • React Native + Expo — cross-platform from day one, web included
  • Supabase — Postgres, auth, edge functions, RLS, all in one
  • Stripe — payments, no thinking required
  • Vercel — deploy on push
  • Claude Haiku — the AI advisor ("Sage")

No custom backend. Just Supabase edge functions for anything touching money.

The Data Model

The hardest part was the data, not the code. 410+ Canadian credit cards, each with category reward rates, signup bonuses, annual fees, FX rates, and issuer-specific rules.

cards (id, name, issuer, annual_fee, fx_rate, ...)
category_rewards (card_id, category, reward_rate, reward_type)
signup_bonuses (card_id, bonus_points, min_spend, timeframe_days)
profiles (user_id, spending_profile jsonb, wallet card_ids[])
Enter fullscreen mode Exit fullscreen mode

The spending_profile JSONB blob lets me calculate optimal reward allocation in a single Postgres query:

SELECT
  c.name,
  SUM(cr.reward_rate * sp.monthly_spend) as estimated_monthly_rewards
FROM cards c
JOIN category_rewards cr ON cr.card_id = c.id
JOIN jsonb_each_text(user_spending_profile) sp ON sp.key = cr.category
GROUP BY c.id, c.name
ORDER BY estimated_monthly_rewards DESC;
Enter fullscreen mode Exit fullscreen mode

Supabase Edge Functions for Stripe

All Stripe logic lives in edge functions — never in the client.

create-checkout:

const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY"));
const user = await supabase.auth.getUser(token);

const session = await stripe.checkout.sessions.create({
  customer_email: user.data.user?.email,
  mode: "subscription",
  line_items: [{ price: priceId, quantity: 1 }],
  success_url: successUrl,
  metadata: { userId: user.data.user?.id },
});

return new Response(JSON.stringify({ url: session.url }));
Enter fullscreen mode Exit fullscreen mode

Gotcha: deploy with --no-verify-jwt and validate tokens manually using the service role client. Trips everyone up the first time.

stripe-webhook updates subscription status in real-time:

const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);

if (event.type === "customer.subscription.updated") {
  await supabase.from("subscriptions").upsert({
    user_id: sub.metadata.userId,
    status: sub.status,
    plan: sub.items.data[0].price.id,
    current_period_end: new Date(sub.current_period_end * 1000).toISOString(),
  });
}
Enter fullscreen mode Exit fullscreen mode

sage-chat-stream streams Claude Haiku with your wallet + spending profile in context:

const stream = await anthropic.messages.stream({
  model: "claude-haiku-20240307",
  max_tokens: 1024,
  system: buildSystemPrompt(userCards, spendingProfile),
  messages: [{ role: "user", content: userMessage }],
});

return new Response(stream.toReadableStream(), {
  headers: { "Content-Type": "text/event-stream" },
});
Enter fullscreen mode Exit fullscreen mode

Sage answers "Should I use my Cobalt or Sapphire at this restaurant?" with actual numbers from your wallet.

RLS That Actually Works

Key lesson: both anon and authenticated need explicit SELECT on public reference tables.

CREATE POLICY "cards_public_read" ON cards
  FOR SELECT TO anon, authenticated USING (true);

CREATE POLICY "profiles_own_data" ON profiles
  FOR ALL USING (auth.uid() = user_id);
Enter fullscreen mode Exit fullscreen mode

Forget to grant anon access and your landing page breaks silently. Fun to debug at 11pm.

CSV Statement Parser

Import your bank statement and instantly see your spending profile. I support 8 Canadian bank formats (TD, RBC, Scotia, BMO, CIBC, Amex, Tangerine, Capital One) — each with different layouts, date formats, and amount sign conventions.

Everything normalizes into:

interface Transaction {
  date: Date;
  merchant: string;
  amount: number; // always positive
  category: SpendingCategory; // regex-classified
}
Enter fullscreen mode Exit fullscreen mode

Fast enough to classify 12 months of transactions client-side with no API call.

Freemium + Lifetime Deal

  • Free — core optimizer, 3 AI messages/month
  • Pro ($5.99/mo) — advanced analytics
  • Max ($12.99/mo) — everything + CSV import + full AI
  • Lifetime ($49.99) — one-time deal, permanent Pro access

The lifetime deal is a Stripe one-time product (not a subscription). The webhook sets is_lifetime: true on the profile and bypasses all subscription checks.

Affiliate links on 7 screens with UTM tracking monetize the free tier — clicking "Apply Now" is how I make money on free users.

What I'd Do Differently

Fewer cards at launch. 410 was ambitious. Data quality issues haunted me for weeks. 50 perfect cards beats 410 imperfect ones.

Next.js for the SEO shell. The Expo web build works, but static HTML outperforms it in search. I'd use Next.js for the marketing layer and keep React Native for the app.

Cache invalidation upfront. I ended up with cards_cache_v4_ prefix I bump manually. Technical debt I'd plan for from day one.

The Numbers

  • 1,187 passing tests across 39 suites before launch
  • 410+ cards, 45 fully verified with all rates
  • 8 bank CSV formats supported
  • 14 days from idea to production

Rewardly is live at rewardly.ca — free to use, no credit card required.

If you're Canadian with a wallet full of cards you're not optimizing, Sage will tell you exactly how much you're leaving on the table.

Roadmap: receipt scanning, Apple Wallet integration, open banking for automatic import.

Top comments (0)