/credit-metering
Usage credits on an append-only ledger with reserve, capture, release and refund.
Then type /credit-metering in Claude Code, or describe the task and let it pick the skill.
What you get back recorded run · 10 Oct 2026
- 10 same-ID requests → 1 charge
- 30 scans vs 5 credits → 5 run
- 25 tests pass
- Scan credits are built: Plus gets 50 scans per paid billing month, one scan costs one credit, people can buy a 100-scan top-up, and a double-click or retry is charged once. The OCR call is still a stub. All 25 tests pass against a local Postgres 16,
next buildsucceeds, and I ran an end-to-end check through the real routes. - 30 simultaneous scans against 5 credits: exactly 5 succeed.
- In the end-to-end run, 5 parallel requests with one scan ID produced one charge (one 409 "already running", four 200s), and a repeated webhook delivery returned
duplicate. - There's no auth in the app.
lib/auth.jsreturns the user from theDEV_USER_IDenv var, and only outside production.
Input → output from the run
const rows = [ ["Groups", "1", "Unlimited", "Unlimited", "Unlimited"], ["Members per group", "4", "20", "100", "Unlimited"], ["Receipt scanning", "—", "50 / month", "Unlimited", "Unlimited"], ["Multi-currency", "—", "✓", "✓", "✓"], ["Recurring bills", "—", "✓", "✓", "✓"], ["Export to CSV", "—", "—", "✓", "✓"], ["Priority support", "—", "—", "—", "✓"], ];
// Reserve one credit for a scan identified by the client's idempotency key.
// Returns { replay: true, request } for an already-succeeded key.
export async function reserveScan(userId, idempotencyKey) {
return tx(async (c) => {
const acct = await lockAccount(c, userId);
await expireAllowanceIfDue(c, acct);
await releaseStale(c, acct);
const existing = (
await c.query("select * from scan_requests where user_id = $1 and idempotency_key = $2", [userId, idempotencyKey])
).rows[0];
if (existing?.status === "succeeded") return { replay: true, request: existing };
if (existing?.status === "pending") throw new ScanInProgress("This scan is already running");When to use it
Use when an app meters consumable usage with credits.
Skip it for plain subscriptions with nothing consumable to meter; payments-with-dodo covers billing itself.
The thinking behind itCredits are money, so treat them like a ledger. Reserve, capture and refund cleanly, and usage stays fair even with retries and parallel requests.
Output contract from the skill's Output section
Report the denomination and policies, schema/migrations, ledger operations and invariants, protected usage paths, provider/product mapping, UI reconciliation, verification evidence, and remaining dashboard or production setup.
How it checks its own work from the instructions
- Reserve, capture, and release flows are tested
- Ledger and balance invariants hold under concurrent spend
- Payment replay and out-of-order events are tested
- Refunds, clawbacks, and promo races are tested
Requirements what it needs
- Works in
- Codex, Claude Code, Cursor
- Needs
- project files, terminal commands, network access
The instructions SKILL.md · 36 lines
Instructions & references
Back to overview ↑On this page 4 sections
Credit metering
Build credits as a financial state machine. Preserve existing denominations, billing semantics, and provider integrations.
Workflow
- Inspect backend/runtime, database/ORM, auth and tenant keys, current balance/ledger fields, payment provider, usage paths, retries, queues, and frontend state.
- Infer existing costs, grants, and packs from code. Ask only for unresolved commercial policy: denomination, per-action or measured cost, free/recurring grants, expiration/rollover, refund/clawback behavior, and overspend/debt policy. Never default to a new payment provider when one exists.
- Read database schemas, then implement only the matching database variant. Do not treat PostgreSQL, MySQL/PlanetScale, Prisma providers, and MongoDB as interchangeable.
- Use an append-only ledger with a unique idempotency key/external event ID. Update cached balance and ledger in one database transaction, or derive balance from the ledger when scale permits. Enforce positive input amounts and the chosen non-negative/debt rule in the database.
- Implement explicit operations:
grantfor signup, subscription, promotion, or admin credit;reservebefore costly/concurrent work;captureafter measured success;releaseon cancellation/failure;refundorreverselinked to the original transaction;expire/rolloverwhen credits have lifecycle rules.
- Authenticate and authorize from server context. Never accept balance, price, pack value, or user identity from the browser. Map a server-owned product/price ID to the credit amount.
- Read payment providers only for the detected provider. Verify raw-body signatures, durably deduplicate events, and retry transient failures. Acknowledge before processing only after a durable queue/inbox write.
- Make promotions atomic. Use a redemption table with a unique
(promo_id, user_id)constraint when one redemption per user is intended; update global usage and grant credit in the same transaction. - Treat the UI balance as a projection. Reconcile after responses, account switches, multi-tab updates, refunds, and webhook grants. Optimistic display must not authorize work.
Verification
Test signup/recurring grant, reserve-capture, reserve-release, insufficient balance, variable final cost, duplicate request, concurrent spend, payment replay, out-of-order event, refund/clawback, promo race, account switching, and reconciliation after network failure. Assert ledger/balance invariants and run database concurrency tests plus repository lint/type/test/build commands.
Output
Report the denomination and policies, schema/migrations, ledger operations and invariants, protected usage paths, provider/product mapping, UI reconciliation, verification evidence, and remaining dashboard or production setup.
Credit database variants
Read this reference after detecting the database. Implement one compatible variant and keep all balance/ledger mutations transactional.
Contents
Shared invariants
- Amounts are positive integers at the operation boundary; ledger deltas carry the sign.
- Every logical operation has a unique idempotency key.
- Ledger rows are append-only and link reversals/refunds to the original operation.
- Balance and ledger update in one transaction, or balance is derived from the ledger.
- Reservation status supports pending/captured/released where concurrent or variable-cost work exists.
- Database constraints enforce the chosen no-debt/debt policy.
PostgreSQL and Supabase
alter table public.users
add column if not exists credit_balance bigint not null default 0,
add constraint users_credit_balance_nonnegative check (credit_balance >= 0);
create table public.credit_transactions (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.users(id),
idempotency_key text not null unique,
kind text not null check (kind in (
'grant', 'reserve', 'capture', 'release', 'refund', 'reverse', 'expire', 'rollover'
)),
amount bigint not null check (amount <> 0),
balance_after bigint not null,
original_transaction_id uuid references public.credit_transactions(id),
external_reference text,
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now()
);
create index credit_transactions_user_created_idx
on public.credit_transactions (user_id, created_at desc);
create unique index credit_transactions_external_reference_idx
on public.credit_transactions (external_reference)
where external_reference is not null;
Implement a database function or application transaction that locks/conditionally updates the balance and inserts the ledger row atomically. For spend/reserve, use a conditional update such as ... where credit_balance >= amount and confirm one affected row inside the transaction.
Apply RLS/grants based on the real auth model. Users may read their own history; only trusted server paths should mutate financial rows.
Prisma
Model the same fields and unique constraints in the provider-compatible Prisma schema. Use an interactive transaction for conditional balance update plus ledger insert. Raw conditional SQL may be necessary for strict atomic spend; do not implement read-then-write in separate calls. JSON defaults and UUID generation differ by database provider, so avoid copying PostgreSQL annotations into MySQL.
MySQL and PlanetScale
Use MySQL-compatible types (char(36)/binary UUID choice, json, datetime) and migrations supported by the project. gen_random_uuid(), jsonb, partial indexes, and ADD COLUMN IF NOT EXISTS are PostgreSQL-specific. Verify foreign-key support/configuration on the actual PlanetScale project. Preserve idempotency with unique indexes and use a transaction or single conditional update supported by the database.
MongoDB
Store balance on the user/account document and ledger in a separate collection with unique indexes on idempotencyKey and optional externalReference. Use a replica-set transaction for balance+ledger atomicity, or a rigorously designed single-document ledger/balance model. A transaction schema alone is insufficient.
Verification
- Concurrent spends cannot cross the permitted balance boundary.
- Replaying an idempotency key returns the original result without another delta.
- Ledger and cached balances reconcile after grants, capture/release, and refund.
- Mutation attempts from ordinary client credentials are denied.
- Migration applies from a clean database and upgrades an existing fixture safely.
Credit payment providers
Read this reference after detecting the existing provider and installed SDK version. Implement one provider path and verify API/event names against current primary documentation.
Contents
Shared checkout contract
The browser submits only a server-owned pack key. The server maps that key to provider price/product ID, currency, and credit amount.
POST /api/billing/credit-checkout
authenticated body: { pack: "starter" }
server lookup: starter -> provider product/price -> 100 credits
Attach stable internal account/user ID and pack key as provider metadata. Never accept credits, amount_cents, price IDs outside an allow-list, or user identity as authoritative browser input.
Stripe
Use the installed Stripe SDK’s Checkout Session pattern and a server-owned price mapping. Process the current successful-payment event appropriate to the checkout mode and payment status. Verify stripe-signature over the raw body with the endpoint secret. Store both event ID and payment/session reference as unique identifiers where useful.
Lemon Squeezy
Use the current Lemon Squeezy checkout API/SDK with a server-owned variant mapping and custom_data containing stable account/user ID plus pack key. Verify the webhook signature exactly as current primary docs specify and handle only the paid/order state that guarantees funds.
Dodo Payments
Use the current official dodopayments SDK and Checkout Sessions API with a server-owned product_cart. Store account/user ID plus pack key in metadata. Prefer the official SDK webhook verification helper and current event guide. Dodo also has provider-managed credit capabilities; decide deliberately whether the application ledger or Dodo wallet is the source of truth—do not update both without reconciliation.
Webhook processing
- Read the raw body and verify signature/timestamp with the provider-supported helper.
- Insert a webhook-inbox/event row with a unique provider event ID.
- If processing synchronously, grant credits and mark the event complete in a database transaction. Return non-2xx for transient failure so the provider retries.
- If acknowledging immediately, first durably enqueue/store the event, then return success and process with retry/dead-letter monitoring.
- Resolve pack value from the server-owned mapping, not mutable metadata credit amounts alone. Cross-check product/price, currency, amount, payment status, and environment.
- Grant with the payment/event reference as the ledger idempotency key.
- Handle refunds/disputes according to the explicit clawback/debt policy and link reversal entries to the purchase.
Do not “always return 200” after a database error in a synchronous handler; that permanently discards the provider’s retry opportunity.
Verification
- Unknown/tampered pack keys are rejected before checkout.
- Test and live product IDs cannot cross environments.
- Valid, invalid, duplicate, concurrent, and out-of-order events behave deterministically.
- A transient database failure is retried or remains in a durable queue.
- Refund/dispute behavior matches the documented balance policy.
- Ledger, cached balance, provider payment, and UI reconcile.
Claude app, ChatGPT and ZIP download
local coding agent required
This skill requires project files, terminal commands, and network access. Uploading it to a chat app does not provide equivalent execution.
ChatGPT Skills
This workflow needs a local coding environment or capabilities that a chat-only Skills upload does not provide.
Package details
- Package
- credit-metering/SKILL.md
- Author
- @tushaarmehtaa
- Added
- 6 Mar 2026
- Tags