All posts

28 Oct 2026 · Auth & Stripe

Designing a Shared Team Credit Pool for AI Workspaces in Next.js 15

Deduct LLM usage from an organization balance instead of a personal counter, with Prisma, Stripe seats, and a debit that survives retries.

B2B buyers do not want each teammate on a personal token piggy bank. They want one workspace balance, roles, and a bill that matches seats plus usage.

AgenticPacks OSS tracks a personal credit counter next to streaming chat: Agentic-Packs/agenticpacks-oss. Pro is organization workspaces, RBAC, 3-tier Stripe, proration, and a token-to-dollar ledger (prompt, completion, Tavily). Compare plans on AgenticPacks Pro.

Rows you actually need

model Organization {
  id      String @id @default(cuid())
  credits Int    @default(0)
}

model Membership {
  organizationId String
  userId         String
  role           Role   // OWNER ADMIN MEMBER
  @@id([organizationId, userId])
}

model UsageLedger {
  id             String   @id @default(cuid())
  organizationId String
  userId         String
  model          String
  promptTokens   Int
  completionTokens Int
  usdMicros      Int
  createdAt      DateTime @default(now())
}

The pool is Organization.credits. People are Membership. The ledger is append-only. Do not decrement a user row for a team product.

Debit first, then call the model

const updated = await prisma.organization.updateMany({
  where: { id: orgId, credits: { gte: estimate } },
  data: { credits: { decrement: estimate } },
});
if (updated.count !== 1) {
  throw new Error("insufficient_credits");
}

updateMany with gte is an atomic check. Two members cannot oversell the pool. After the stream ends, write UsageLedger and adjust if the estimate was high (credit back) or low (debit the rest). Never wait until after OpenAI returns to take the lock. A failed HTTP retry would skip the debit or double it.

Idempotency for billing events is a sibling problem: Stripe webhooks.

Stripe maps to the org, not the user

Checkout client_reference_id or metadata should be organizationId. checkout.session.completed grants pool credits. Seat changes go through customer.subscription.updated. The person who pays can be OWNER. Members spend the same pool.

Do not create one Stripe customer per user in a team plan. You will never reconcile.

Server Actions and the agent route

Every spend path (chat, ingest, LangGraph) calls the same debit(orgId, estimate). Authz is membership plus role. A MEMBER can spend. An OWNER can buy. That guard is the same pattern as Server Actions IDOR.

Prometheus can scrape remaining credits and USD burn. See token tracking with Grafana.

Tired of a personal credit integer pretending to be B2B billing?

Start with the personal counter in Agentic-Packs/agenticpacks-oss.

Team pools, seats, and the ledger are in team credit pools in Pro.

What founders argue about

ChoiceDefault that works
Hard stop at 0Yes. Soft debt is support tickets.
Per-member capsOptional later. Pool first.
Top-up vs subscription grantSubscription grants monthly. Top-up is a Checkout.
Show USD or tokensBoth. Engineers want tokens. Finance wants USD.

Proportional billing (prompt vs completion vs search fee) is why a single Stripe quantity is not enough. The ledger line is the product.


Where to Go From Here

You do not have to discover org-scoped debit on a live customer.

Option A: Free open-source baseline (MIT)


Option B: Ship with AgenticPacks Pro ($149 one-time)

If the buyer is a team:

  • Automated Hetzner provisioning: Terraform, Caddy, Compose.
  • Production multi-agent core: LangGraph, pgvector, Redis, workspace-scoped threads.
  • Usage & Unit Economics: 3-tier Stripe, portal, proration, team credit pools, token-to-dollar ledger.