# レシピ：API クレジット

> API や AI 機能の前払いクレジットを販売し、リクエストごとに 1 つ差し引きます。残高が足りなければ、チャージのリンク付きで 402 を返します。

購入者はクレジットパックを前払いし、リクエストごとにいくつか消費します。記録は Perly Pay が行います：消費はマイナスにならないアトミックな 1 回の呼び出しで、購入者ごとに専用のチャージページがあります。自動で請求されることはなく、クレジットがなくなれば購入者がチャージします。クレジットのすべては [Sell credits](https://pay.perly.io/docs/credits.md) に。ここではそれを使ったレシピを紹介します。

> **注:** アシスタントと一緒に：このレシピは、Perly Pay プラグインの `sell-credits` スキルがあなたのプロジェクトで実行します。動くサンプルは、プラグインのリポジトリの `examples/api-credits`（MIT）です。

## 仕組み

1. **クレジットパック** — `model: "credit_pack"`、セント単位の `unit_amount`、`credits` を持つ商品と価格。モードごとに 1 回。
2. **ユーザーが顧客に** — `external_id`（あなたのユーザー ID）で一度作成。以降、`credits.*` はその ID を受け付けます。
3. **チャージ** — `credits.topUpUrl(userId)` は購入者専用のページ：パックを選んで支払います。支払い済みのパックごとにクレジットが加算されます。
4. **リクエストごとに消費** — まず `credits.consume(userId, cost, { idempotencyKey })`、それから処理。同じキーでの再試行は最初の消費を返すため、キーはリクエストごとにサーバーが作り、呼び出し元からは決して受け取りません。
5. **足りない？** — `insufficient_credits`（409）は何も差し引きません：500 ではなく、チャージページ付きで `402` を返してください。

**Spend one request's credits**

```js
try {
  await pay.credits.consume(userId, cost, { product: PRODUCT, action: "api", idempotencyKey: `req-${userId}-${requestId}` });
} catch (e) {
  if (e.code !== "insufficient_credits") throw e;
  return reply(402, { top_up: await pay.credits.topUpUrl(userId, { product: PRODUCT }) });
}
// now do the work
```

## サンプルを動かす

1. **インストール** — `examples/api-credits` で `npm install`（Node 20.12+）。
2. **キー** — `.env.example` を `.env` にコピーし、`PERLY_PAY_SECRET_KEY` にテストキー（`sk_test_…`）を書きます。
3. **準備** — `npm run setup` があなたのキーで商品と価格を作成し、`.env` 用にその ID を表示します。
4. **起動** — README のとおり `npm start`（または `npm run dev`）。
5. **ウォレットなしで支払う** — `POST /v1/shout` を呼び（402）、`POST /top-up` が返すページを開き、パックを選び、請求書で **Simulate payment** を押してもう一度呼びます：200 と `X-Credits-Remaining: 499`。

## 落とし穴

- 冪等キーなしの消費：再試行されたリクエストで二重に差し引かれます。
- クライアントが送るもの（`Idempotency-Key` ヘッダー）を消費のキーにすること：同じキーは最初の消費を成功として返し、クライアントは支払わずに再び処理を受けられます。
- 残高を確認してから消費する 2 段階にすること。`consume` 自体がマイナスを防ぎます。残高は表示用です。
- 処理してから消費すること。逆です：まず消費。
- 本番キーでテストの ID を使うこと。商品、価格、Webhook はひとつのモードに属します：公開前に `sk_live_…` で設定をやり直してください。
