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
| Choice | Default that works |
|---|---|
| Hard stop at 0 | Yes. Soft debt is support tickets. |
| Per-member caps | Optional later. Pool first. |
| Top-up vs subscription grant | Subscription grants monthly. Top-up is a Checkout. |
| Show USD or tokens | Both. 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)
- GitHub: github.com/Agentic-Packs/agenticpacks-oss
- What is included: personal credit counter, streaming chat, single-plan Stripe, Auth.js, Postgres. No team pool.
- Cost: free, 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.
