golemkit

AI & billing

Payments

Users buy credit packs (one-time payments), defined in src/modules/billing/plans.ts. Payments are provider-agnostic behind the PaymentProvider interface — Stripe (global) and iyzico (Turkey) ship in the box, selected with one env var:

PAYMENT_PROVIDER="stripe"   # or "iyzico"

Billing stays disabled (UI shows a notice, API returns 501) until the chosen provider’s keys are set — the app never breaks for lack of a payment account.

Stripe

  1. Set STRIPE_SECRET_KEY (test keys work).
  2. Create a webhook endpoint for checkout.session.completed pointing at /api/stripe/webhook, and set STRIPE_WEBHOOK_SECRET. Locally, stripe listen --forward-to localhost:3000/api/stripe/webhook prints one.

Flow: checkout → hosted payment page → webhook verifies the signature and grants credits.

iyzico

  1. Set IYZICO_API_KEY and IYZICO_SECRET_KEY (sandbox keys from sandbox-merchant.iyzipay.com; IYZICO_URI defaults to the sandbox API).
  2. No webhook setup needed — iyzico’s Checkout Form POSTs a token back to /api/iyzico/callback, which retrieves and verifies the payment.

Before going live: the shipped provider charges the pack’s USD number as TRY (₺20 for the $20 pack) — placeholder pricing that’s fine in the sandbox and wrong with real money. It deliberately refuses to run when IYZICO_URI points at the production API; implement real TRY pricing in src/modules/billing/providers/iyzico.ts first.

Subscriptions — the credit hybrid

Besides one-time packs, subscriptionPlan in src/modules/billing/plans.ts defines a monthly plan that auto-grants credits every billing cycle. Stripe-only: checkout uses subscription mode with a dynamically created recurring price (no dashboard products needed), the plan’s metadata is snapshotted onto every invoice, and the webhook grants monthlyCredits on invoice.paid — add that event to your webhook endpoint. The invoice id is the idempotency reference, so Stripe’s retries can’t double-grant.

Why double-grants can’t happen

Both providers grant credits through grantCredits() with the provider’s payment reference. That reference has a unique constraint in the ledger, so retried webhooks and refreshed callbacks are database-level no-ops.

Adding another provider

Implement the PaymentProvider interface in src/modules/billing/providers/ (one method: createCheckoutUrl), add its completion route, and register it in providers/index.ts. Nothing else in the app changes.