> For the complete documentation index, see [llms.txt](https://series-1.gitbook.io/rundot-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://series-1.gitbook.io/rundot-docs/v5.23.0/readme/credits.md).

# Credits API (BETA)

Read the player's AI **Creator credits** and drive a plan paywall. When a game is **player-billed**, the AI generation it triggers (`ai`, `imageGen`, `videoGen`) is charged to the signed-in player's Creator credits instead of the game owner's. This API lets you show the player their balance and plan, and resolve a deficit when they run out.

{% hint style="warning" %}
All SDK methods can reject; unhandled rejections crash the app. Always wrap SDK calls in `try/catch` or attach a `.catch()` handler. See [Error Handling](/rundot-docs/v5.23.0/readme/error-handling.md) for details.
{% endhint %}

> **Note:** Creator credits are **not** the same as RunBucks. RunBucks are the in-game hard currency, handled by the [Purchases API](/rundot-docs/v5.23.0/readme/purchases.md) (`RundotGameAPI.iap`). Creator credits are the AI credits that power generation in player-billed games. This API (`RundotGameAPI.credits`) only deals with Creator credits.

## Is My Game Player-Billed?

A game's billing mode is set by the platform, not by your game. `getBillingContext()` tells you how the current session bills AI generation. It is **informational only** — it never gates the other methods, so you can call any method regardless of billing mode and decide what to render yourself.

```typescript
import RundotGameAPI from '@series-inc/rundot-game-sdk/api'

const ctx = await RundotGameAPI.credits.getBillingContext()
if (ctx.billedTo === 'player') {
  // The player pays for AI generation — show their balance / a top-up entry point.
  if (ctx.playerChargesEnabled) {
    // Charges are actively enforced right now.
  }
}
```

## Quick Start

```typescript
import RundotGameAPI from '@series-inc/rundot-game-sdk/api'

// Show the player's spendable Creator credits.
const balance = await RundotGameAPI.credits.getBalance()
console.log(`${balance.available} / ${balance.total} credits`)

// Let the player upgrade their plan.
const result = await RundotGameAPI.credits.openPaywall()
if (result.outcome === 'purchased') {
  updateCreditsUI(result.balance)
}
```

## Handling Exhaustion

When a player-billed game runs the player out of credits mid-generation, the host can automatically open the paywall and retry the call — so for most games you don't have to do anything. Two flags (both **on** by default) control this:

* `setAutoPaywallOnExhaustion(enabled)` — on exhaustion, automatically open the paywall.
* `setAutoRetryOnPurchase(enabled)` — after a paywall purchase, automatically retry the original generation once and resolve it with the result.

| autoPaywall      | autoRetry        | What happens on exhaustion                                                                                                              |
| ---------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `true` (default) | `true` (default) | Paywall opens. On purchase, the original call retries and resolves with the result. On cancel, it rejects with `CreditsExhaustedError`. |
| `true`           | `false`          | Paywall opens. On purchase, the call **rejects** (`paywallOutcome: 'purchased'`) so you can retry it yourself. On cancel, it rejects.   |
| `false`          | *(ignored)*      | The call rejects immediately (`paywallShown: false`).                                                                                   |

These setters are **synchronous** — they update local SDK state and push it to the host fire-and-forget.

```typescript
// Drive your own credits UX instead of the built-in paywall:
RundotGameAPI.credits.setAutoPaywallOnExhaustion(false)

try {
  const image = await RundotGameAPI.imageGen.generate({ prompt: 'a fox' })
  show(image)
} catch (err) {
  if (isCreditsExhaustedError(err)) {
    // Show your own out-of-credits UI, then open the paywall on demand.
    const result = await RundotGameAPI.credits.openPaywall({ highlightTier: 'plus' })
    if (result.outcome === 'purchased') {
      retryGeneration()
    }
  }
}
```

## Reacting to Balance Changes

`onBalanceChanged` fires when the player's spendable balance changes (for example after a paywall purchase). It returns an unsubscribe function.

```typescript
const unsubscribe = RundotGameAPI.credits.onBalanceChanged((balance) => {
  updateCreditsUI(balance)
})

// later
unsubscribe()
```

## API Reference

<table><thead><tr><th width="320">Method</th><th>Returns</th><th>Description</th></tr></thead><tbody><tr><td><code>getBillingContext()</code></td><td><code>Promise&#x3C;CreditsBillingContext></code></td><td>How the current game bills AI generation. Informational only — never gates other calls.</td></tr><tr><td><code>getBalance()</code></td><td><code>Promise&#x3C;CreditBalance></code></td><td>The player's spendable / total Creator credits and free-daily info.</td></tr><tr><td><code>getSubscription()</code></td><td><code>Promise&#x3C;CreditSubscription></code></td><td>The player's current Creator plan (paid tier), if any.</td></tr><tr><td><code>getPlans()</code></td><td><code>Promise&#x3C;CreditPlansCatalog></code></td><td>The available plans, top-up packs, and the free-daily grant.</td></tr><tr><td><code>openPaywall(options?)</code></td><td><code>Promise&#x3C;CreditsPurchaseResult></code></td><td>Open the host paywall. Resolves once the player closes it.</td></tr><tr><td><code>setAutoPaywallOnExhaustion(enabled)</code></td><td><code>void</code></td><td>Toggle the automatic exhaustion paywall (default <code>true</code>). Synchronous.</td></tr><tr><td><code>getAutoPaywallOnExhaustion()</code></td><td><code>boolean</code></td><td>Current auto-paywall setting.</td></tr><tr><td><code>setAutoRetryOnPurchase(enabled)</code></td><td><code>void</code></td><td>Toggle automatic retry after an exhaustion purchase (default <code>true</code>). Synchronous.</td></tr><tr><td><code>getAutoRetryOnPurchase()</code></td><td><code>boolean</code></td><td>Current auto-retry setting.</td></tr><tr><td><code>onBalanceChanged(listener)</code></td><td><code>Unsubscribe</code></td><td>Subscribe to balance changes. Returns an unsubscribe function.</td></tr></tbody></table>

## Types

These types are exported from the package root, `@series-inc/rundot-game-sdk` (not the `/api` subpath).

### `CreditsBillingContext`

| Field                  | Type                  | Description                                                              |
| ---------------------- | --------------------- | ------------------------------------------------------------------------ |
| `billedTo`             | `'owner' \| 'player'` | Who AI generation in this game is billed to.                             |
| `playerChargesEnabled` | `boolean`             | `true` only when `billedTo === 'player'` AND charge enforcement is live. |

### `CreditBalance`

| Field       | Type                          | Description                                                                         |
| ----------- | ----------------------------- | ----------------------------------------------------------------------------------- |
| `available` | `number`                      | Credits the player can spend right now.                                             |
| `total`     | `number`                      | The "available / total" denominator.                                                |
| `freeDaily` | `CreditFreeDailyInfo \| null` | Free-daily grant info, or `null` when the player isn't eligible (e.g. a paid plan). |

`CreditFreeDailyInfo`: `{ dailyCredits: number; availableCredits: number; nextResetAt: string }` (`nextResetAt` is an ISO timestamp).

### `CreditSubscription`

| Field              | Type                 | Description                                     |
| ------------------ | -------------------- | ----------------------------------------------- |
| `status`           | `'active' \| 'none'` | Whether the player has an active Creator plan.  |
| `tier`             | `string \| null`     | The paid plan tier (e.g. `'plus'`), or `null`.  |
| `monthlyCredits`   | `number \| null`     | Credits granted per period for the active plan. |
| `creditsRemaining` | `number \| null`     | Credits remaining in the current period.        |
| `renewsAt`         | `string \| null`     | ISO renewal/expiry timestamp.                   |
| `willRenew`        | `boolean`            | Whether the plan will auto-renew.               |

### `CreditPlansCatalog`

| Field              | Type                | Description                                              |
| ------------------ | ------------------- | -------------------------------------------------------- |
| `plans`            | `CreditPlan[]`      | Available subscription plans.                            |
| `topUpPacks`       | `CreditTopUpPack[]` | One-time top-up packs (purchase UI is a future release). |
| `freeDailyCredits` | `number`            | The free-daily grant amount.                             |

`CreditPlan`: `{ tier: string; productId: string; monthlyCredits: number; rolloverDays: number }`. `CreditTopUpPack`: `{ productId: string; credits: number }`.

### `OpenPaywallOptions`

| Field           | Type                 | Required | Description                                                                    |
| --------------- | -------------------- | -------- | ------------------------------------------------------------------------------ |
| `focus`         | `'plans' \| 'topup'` | No       | Which surface to focus. `'topup'` is a future surface (currently shows plans). |
| `highlightTier` | `string`             | No       | A tier id to visually highlight in the plan picker.                            |
| `screenName`    | `string`             | No       | Screen/route name for analytics attribution.                                   |

### `CreditsPurchaseResult`

| Field     | Type                                      | Description                                                                       |
| --------- | ----------------------------------------- | --------------------------------------------------------------------------------- |
| `outcome` | `'purchased' \| 'cancelled' \| 'pending'` | What the player did in the paywall.                                               |
| `balance` | `CreditBalance \| null`                   | The refreshed balance after the paywall closed, or `null` if it couldn't be read. |

### `CreditsExhaustedError`

Thrown from `ai` / `imageGen` / `videoGen` calls when a player-billed game runs the player out of credits and the deficit wasn't resolved. Use the exported `isCreditsExhaustedError(err)` / `asCreditsExhaustedError(err)` helpers to detect and narrow it.

| Field            | Type                                              | Description                                                 |
| ---------------- | ------------------------------------------------- | ----------------------------------------------------------- |
| `code`           | `'CREDITS_EXHAUSTED'`                             | Stable discriminator.                                       |
| `billedTo`       | `'owner' \| 'player'`                             | Who the generation was billed to.                           |
| `paywallShown`   | `boolean`                                         | Whether the host opened the paywall before giving up.       |
| `paywallOutcome` | `'purchased' \| 'cancelled' \| 'pending' \| null` | The paywall result (`null` when `paywallShown` is `false`). |

## Best Practices

* Treat `getBillingContext()` as a hint for what to render — never as a gate. Any method works regardless of billing mode.
* Leave the auto-paywall on for the simplest experience; only turn it off when you want a custom out-of-credits UX.
* Use `onBalanceChanged` to keep your credits UI in sync rather than polling `getBalance()`.
* Always handle the `'cancelled'` outcome — players will dismiss the paywall.
