# Credits SDK

<Lead>
Credits sind die Verbrauchs-Währung der Plattform (1 Credit = 0,02 €). Es gibt genau ein Abrechnungs-Muster: **Reserve→Compute→Commit/Refund**. Direkt abbuchen gibt es nicht, in keinem Codepfad.
</Lead>

## In Modulen: der Harness bucht, nicht du

Modul-Code ruft die Credits-Engine **nie selbst** auf. Der Runs-Harness reserviert vor deinem `runTool` die `creditsPerUse` aus dem Manifest, committet bei Erfolg und erstattet bei jedem Fehler:

```typescript
export const runTool: ModuleCompute = async (ctx, { payload }) => {
  // Credits sind hier bereits reserviert.
  const result = await doTheWork(ctx, payload);
  return { output: result };   // Rückgabe → Commit
  // Werfen → Refund + Fehl-Notification. Mehr musst du nicht tun.
};
```

User-Trust-Regel: **jeder technische Fehler erstattet**. Das erledigt der Harness; baue kein eigenes Refund-Handling.

## In Plattform-Code: die CreditsEngine

Server-Code außerhalb des Harness (z.B. eigene Batch-Aktionen) nutzt die Engine direkt. Sie ist eine Factory, Apps halten einen gecachten Handle:

```typescript
import { createCreditsService, InsufficientCreditsError } from "@reosa/sdk";

const credits = createCreditsService(db, { includedCreditsForPlan });

const r = await credits.reserve(ctx, {
  moduleId: "homestaging",
  creditCost: 8,               // 0.1 bis 100000, eine Nachkommastelle
  description: "Batch-Lauf",   // optional, max 300 Zeichen
});
// r: { id, periodId, credits, expiresAt }  · Reservierung verfällt nach 5 Minuten

try {
  const { actualCredits } = await compute();
  await credits.commit(ctx, r.id, actualCredits);  // actualCredits optional
} catch (error) {
  await credits.refund(ctx, r.id, "generation failed");
  throw error;
}
```

Die wichtigsten Eigenschaften:

- **`reserve(ctx, { moduleId, creditCost, description? })`** hält den Betrag im Tenant-Pool. Parallele Aufrufe serialisieren auf der Perioden-Zeile; überbuchen ist unmöglich.
- **`commit(ctx, reservationId, actualCredits?)`** bucht ohne `actualCredits` exakt den reservierten Betrag. Mit `actualCredits` wird **nach unten** abgerechnet: berechnet wird `min(reserviert, actualCredits)`, der Rest wird freigegeben. Mehr als reserviert wird nie berechnet (Worst-Case-Schutz für den Tenant). Gedacht für Batch-Aktionen, die vorab schätzen und die echte Summe erst am Ende kennen.
- **`refund(ctx, reservationId, reason)`** gibt die volle Reservierung frei; `reason` landet im Ledger.
- **Reservierungen verfallen nach 5 Minuten** (`RESERVATION_TTL_MS`). Abgelaufene Reservierungen räumt der nächste `reserve()`-Aufruf lazy ab; ein verspäteter Commit auf eine verfallene Reservierung schlägt fehl.
- **Credits tragen eine Nachkommastelle.** Beträge werden an jeder Grenze mit `roundCredits()` auf 1 Dezimale gerundet (1.55 → 1.6).
- Lese-APIs: `getBalance(ctx)`, `history(ctx, { limit, before })`, `usageSummary(ctx)` (Verbrauch nach Modul und Mitglied).

## Fehlerbilder

Alle Fehler erben von `SdkError`; branche mit `instanceof` oder `code`:

| Fehler | `code` | Bedeutung |
|---|---|---|
| `InsufficientCreditsError` | `INSUFFICIENT_CREDITS` | Der Workspace-Pool deckt den Betrag nicht (und Extended Usage greift nicht). |
| `MemberAllocationExceededError` | `MEMBER_ALLOCATION_EXCEEDED` | Das persönliche Monats-Kontingent des Mitglieds ist erschöpft, obwohl der Pool noch Guthaben hätte. Zeige "dein Kontingent ist aufgebraucht", nicht "Workspace leer". |
| `OverageCapReachedError` | `OVERAGE_CAP_REACHED` | Extended Usage ist aktiv, aber das selbst gesetzte Monats-Budget ist erreicht. Harter Block. |
| `SdkError` | `RESERVATION_NOT_PENDING` | Commit/Refund auf eine Reservierung, die fehlt, schon abgerechnet oder verfallen ist. |

Der Harness übersetzt diese Fehler für die Tool-UI in den `ModuleRunOutcome`-Code `insufficient_credits`; verlinke dort auf die Credit-Verwaltung statt eine rohe Fehlermeldung zu zeigen.

## Anzeige-Regel: kleine Beträge verstecken

Beträge unter `SMALL_CREDIT_COST_THRESHOLD` (= 10 Credits, aus `@reosa/config`) werden in der UI standardmäßig **nicht angezeigt**. Abgebucht wird immer korrekt; nur die Anzeige entfällt.

- Anzeige-Elemente kleiner Beträge tragen die CSS-Klasse `small-credit-cost`. Die globale Stylesheet-Regel blendet sie aus, solange die Nutzer:in die persönliche Einstellung "Kleine Credit-Beträge anzeigen" nicht aktiviert hat. Baue keine eigene Sichtbarkeits-Logik.
- **Buttons nennen die Aktion, nie den Preis:** "KI-Briefing erstellen", nicht "KI-Briefing (2 Credits)".
- Kosten-Bestätigungs-Dialoge nur für wirklich teure Aktionen ab der Schwelle.

## Credit-Tools vs. Add-on-Tools

- **`pricing: "credits"`** (Default): jeder Lauf bucht `creditsPerUse` über den Harness.
- **`pricing: "addon"`**: das Tool wird als Monats-Add-on lizenziert (Preis + enthaltene Voll-Läufe in `TOOL_ADDONS` in `@reosa/config`, optional mit Tiers). Das Kontingent-Limit greift **beim Start** eines Kern-Laufs; ein laufender Lauf wird nie mitten im Flow wegen Credits oder Kontingent gestoppt. Kleinere Einzel-Aktionen des Tools kosten weiterhin normal Credits über den Harness, still und ohne Preis-Anzeige. Der Marketplace zeigt für Add-on-Tools den Add-on-Preis, keine Credit-Labels.

## Weiterführend

Kosten-Schätzung für KI-Läufe: [REOS AI](/dev/sdk/reos-ai) (`estimateCredits`). Der komplette Modul-Vertrag: [Modul-Guide](/dev/module-guide).
