# 레시피: API 크레딧

> API나 AI 기능용 선불 크레딧을 팔고 요청마다 하나씩 차감하세요. 잔액이 부족하면 충전 링크와 함께 402로 응답합니다.

구매자는 크레딧 패키지를 미리 결제하고, 요청마다 몇 개씩 씁니다. 장부는 Perly Pay가 맡습니다: 차감은 마이너스를 허용하지 않는 원자적 호출 하나이고, 구매자마다 자기 충전 페이지가 있습니다. 자동으로 청구되는 것은 없습니다 — 크레딧이 떨어지면 구매자가 충전합니다. 크레딧의 모든 것은 [Sell credits](https://pay.perly.io/docs/credits.md)에 있고, 여기는 그 둘레의 레시피입니다.

> **참고:** 어시스턴트와 함께: 이 레시피는 Perly Pay 플러그인의 `sell-credits` 스킬이 내 프로젝트에서 실행합니다. 실행 가능한 예제는 플러그인 저장소의 `examples/api-credits`(MIT)입니다.

## 작동 방식

1. **크레딧 패키지** — `model: "credit_pack"`, 센트 단위 `unit_amount`, `credits`가 있는 상품과 가격. 모드당 한 번.
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` 헤더)에서 차감 키를 가져오기: 같은 키는 첫 차감을 성공으로 돌려주므로 클라이언트가 결제 없이 작업을 또 받습니다.
- 잔액을 확인한 뒤 차감하는 두 단계. `consume`이 스스로 마이너스를 막습니다. 잔액은 표시용입니다.
- 작업 먼저, 차감 나중. 반대로: 차감 먼저.
- 라이브 키에 테스트 ID. 상품, 가격, 웹훅은 한 모드에 속합니다: 출시 전에 `sk_live_…`로 설정을 다시 하세요.
