DEV Community

Amine Charot
Amine Charot

Posted on AI-assisted

Payments are not entitlements

When you build a SaaS, you wire Stripe and think plans are done. Then the first paid feature ships and you need to answer, on every request: may this user do this right now?

That question is not payments. It is entitlements.

Two layers, two jobs

Layer Job Example
Payments Charge the card, manage the subscription Stripe, Paystack, Paddle
Entitlements Decide whether this request runs under the plan UsageGate

Stripe knows a customer pays for Pro. It does not know that Pro includes 500 AI credits, that this user has spent 499, and that two requests just arrived at the same millisecond. Billing meters record usage for the invoice. They don't block anything.

What building entitlements yourself means

  • A ledger of balances per user and feature, with atomic decrements so two requests cannot spend the same credit.
  • Monthly renewals on each plan's period, plus a free tier that refills on its own clock.
  • Webhook handling for upgrades, downgrades, cancels, retries, and events that arrive out of order.
  • A fast check on every request, with a cache and a fallback when it is down.

Every SaaS team writes this once, badly, then rewrites it.

The stack

SaaS stack: Next.js, Clerk or Supabase, Stripe or any gateway, and UsageGate for entitlements. Don't build the ledger yourself.

Layer Use
App Next.js (App Router)
Auth Clerk or Supabase
Database Supabase
Email Resend
Payments Stripe, Paystack, Paddle, or other
Entitlements UsageGate

What it looks like in code

import { GateClient } from "@usagegate/sdk";

const gate = new GateClient(process.env.USAGEGATE_KEY!);

// on signup
await gate.grantPlan(user.id, "plan_free");

// in the expensive route
if (!(await gate.canAccess(user.id, "ai_credits"))) {
  return Response.json({ error: "upgrade" }, { status: 402 });
}
const result = await generate();
await gate.consume(user.id, "ai_credits", 1);
Enter fullscreen mode Exit fullscreen mode

With Stripe, Checkout sets subscription_data.metadata.end_user_id to the same user id. When the customer pays, the webhook assigns the plan that matches the Price. When they cancel, they return to Free.

Using an AI agent to build it

Paste the rule from https://www.usagegate.io/docs/ai-setup into AGENTS.md or your Cursor rules. Every agent in the repo then uses the entitlements layer instead of inventing a ledger.

Top comments (0)