ship-credits について
ship-credits は、既存アプリにクレジットシステムを構築するコーディングスキルです。プロジェクトを調査し、技術スタックとユーザーの選択に合わせてデータベース、バックエンド、決済、フロントエンドを整えます。
主な機能
- 01プロジェクトの技術スタック調査
- 02クレジットスキーマと取引台帳
- 03残高確認と消費ミドルウェア
- 04決済 Webhook の統合
- 05フロントエンド状態と UI
- 06プロモコードと管理ツール
活用シーン
- 01既存アプリへの有料クレジット追加
- 02クレジット変動の記録と監査
- 03残高不足時の操作ブロック
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tushaarmehtaa/ tushar-skills --skill ship-creditsインストール後、Claude Code で/ship-creditsを入力して読み込まれたか確認します。
同梱ファイルreferences/database-schemas.mdreferences/payment-providers.md
詳細
クレジットシステム一式(データベーススキーマ、バックエンドミドルウェア、決済 Webhook、フロントエンドの状態管理、UI コンポーネント)を構築します。最初にプロジェクトを読み込み、既存の構成を活用します。
フェーズ 1:プロジェクトを把握する
コードを書く前に、コードベースを読み、次の質問に答えてください。
1.1 技術スタックを特定する
- バックエンド:Next.js API routes/FastAPI/Express/Django/Rails?
- データベース:Supabase/Postgres/PlanetScale/MongoDB/Prisma?
- 認証:Clerk/NextAuth/Supabase Auth/Firebase Auth/カスタム JWT?
- フロントエンドの状態管理:Zustand/Redux/React Context/Jotai/標準機能?
- 既存の決済機能:Stripe/Lemon Squeezy/Dodo/Paddle のインポートがあるか確認します。
1.2 ユーザーに確認する
構築に着手する前に、次の選択事項を確認してください。
I'll set up credits for your [framework] app with [database].
Quick decisions needed:
1. What costs credits? (e.g., "AI generation = 5, image gen = 4, export = 2")
2. Free tier: How many credits on signup? (e.g., 50)
3. Payment provider preference? (Stripe / Lemon Squeezy / Dodo / manual only)
4. Credit pack pricing? (e.g., "$5 = 100 credits, $10 = 250 credits")
Defaults: 50 free credits, per-action costs you define, Stripe.
フェーズ 2:データベーススキーマ
references/database-schemas.md を読み、特定したデータベースに合致するバリエーションのみを実装します。
すべてのバリエーションで、次の要件を満たす必要があります。
- ユーザーレコードに負の値にならない残高を保持する
- 変更不可のクレジット取引監査ログを保持する
- ユーザー別および時間別の取引検索用インデックスを設定する
- 残高が 0 未満にならないよう、残高変更をアトミックに行う
- ユーザーが希望した場合に限り、プロモコードの保存機能を追加する
取引台帳の準備ができたらユーザーに伝え、変更したマイグレーションまたはスキーマファイルの名前を知らせてください。
フェーズ 3:バックエンドのクレジットサービス
次の主要関数を含むクレジットサービスモジュールを作成します。プロジェクトの言語とフレームワークに合わせてください。
主要関数
get_credits(user_identifier) — 現在の残高を取得します。
Input: auth_id or email (whatever the project uses to identify users)
Output: integer (credit balance) or null (user not found)
Logic: Query users table, return credits column
deduct_credits(user_identifier, amount, reason) — クレジットを消費します。
Input: user identifier, amount to deduct, reason string
Output: boolean (success/failure)
Logic:
1. Get current credits
2. If credits < amount → return false (DO NOT go negative)
3. Update users.credits = credits - amount
4. Insert credit_transactions record with negative amount
5. Return true
add_credits(user_identifier, amount, reason, metadata) — クレジットを追加します。
Input: user identifier, amount to add, reason, optional metadata (payment_id, promo_code, etc.)
Output: new balance
Logic:
1. Update users.credits = credits + amount
2. Insert credit_transactions record with positive amount
3. Return new balance
重要な実装上の詳細:
- 差し引く前に必ず残高を確認してください。フロントエンドを信用してはいけません。
- データベースレベルの制約またはトランザクションを使って、競合状態を防止してください。
- 高スループットの場合は、
UPDATE users SET credits = credits - $amount WHERE id = $id AND credits >= $amountを使い、影響を受けた行数を確認してください。 - 必ず credit_transactions に記録してください。これが金銭取引の監査ログになります。
クレジット確認ミドルウェア
クレジットを消費するエンドポイントの前に実行されるミドルウェアを作成します。
Next.js API routes の場合:
// middleware pattern — adapt to project's auth
export function withCredits(handler, cost) {
return async (req, res) => {
const user = await getAuthUser(req); // from project's auth
const credits = await getCredits(user.id);
if (credits < cost) {
return res.status(402).json({
error: 'Insufficient credits',
required: cost,
balance: credits,
});
}
// Attach to request for handler to use
req.creditsCost = cost;
req.deductCredits = () => deductCredits(user.id, cost, 'api_action');
return handler(req, res);
};
}
FastAPI の場合:
async def require_credits(amount: int):
async def dependency(request: Request):
user = await get_current_user(request) # from project's auth
credits = get_credits(user.id)
if credits is None or credits < amount:
raise HTTPException(
status_code=402,
detail={"error": "Insufficient credits", "required": amount, "balance": credits or 0}
)
return user
return Depends(dependency)
Express の場合:
function requireCredits(cost) {
return async (req, res, next) => {
const credits = await getCredits(req.user.id);
if (credits < cost) {
return res.status(402).json({ error: 'Insufficient credits', required: cost, balance: credits });
}
req.creditsCost = cost;
next();
};
}
クレジット不足時の適切なステータスコードは HTTP 402 です。 文字どおり「Payment Required」を意味します。エラーハンドラーで明示的に処理し、汎用の 4xx/5xx ハンドラーに捕捉させないでください。
クレジットコストの設定
各項目のコストを一元管理する単一の情報源を作成します。
// config/credits.ts (or equivalent)
export const CREDIT_COSTS = {
// Define based on user's answer from Phase 1
GENERATE: 5,
REGENERATE: 1,
IMAGE: 4,
EXPORT: 2,
} as const;
export const FREE_CREDITS = 50;
export const CREDIT_PACKS = [
{ credits: 100, price_cents: 500, label: '$5' },
{ credits: 250, price_cents: 1000, label: '$10' },
{ credits: 600, price_cents: 2000, label: '$20' },
] as const;
フェーズ 4:決済の統合
references/payment-providers.md を読み、フェーズ 1 で選択したプロバイダーのみを実装します。プロジェクトにすでに決済プロバイダーがある場合、別のプロバイダーを追加してはいけません。
すべてのプロバイダー実装で、次の要件を満たす必要があります。
- ユーザー識別子とクレジット数をチェックアウトのメタデータに含める。
- 生のリクエストボディを使って Webhook 署名を検証する。
- プロバイダーの決済 ID またはイベント ID を使って冪等性を確保する。
- 決済の検証後にのみ、クレジットを追加して取引台帳に記録する。
- 処理の失敗をログに記録しつつ、リファレンスに記載されたプロバイダー向けの安全な確認応答を返す。
フェーズ 5:フロントエンドのクレジット状態管理
Zustand ストア(React での推奨)
import { create } from 'zustand';
interface CreditsStore {
credits: number | null; // null = not loaded yet
setCredits: (credits: number | null) => void;
deduct: (amount: number) => void;
}
export const useCreditsStore = create<CreditsStore>((set) => ({
credits: null,
setCredits: (credits) => set({ credits }),
deduct: (amount) =>
set((state) => ({
credits: state.credits !== null ? Math.max(0, state.credits - amount) : null,
})),
}));
クレジット同期フック
// hooks/useCredits.ts
export function useCredits() {
const { credits, setCredits, deduct } = useCreditsStore();
// Fetch on mount (after auth)
const fetchCredits = async () => {
const res = await fetch('/api/user/credits');
const data = await res.json();
setCredits(data.credits);
};
// Optimistic deduction — update UI immediately, backend confirms async
const spendCredits = async (amount: number, action: () => Promise<void>) => {
if (credits !== null && credits < amount) {
// Trigger low credit UI
return { success: false, reason: 'insufficient' };
}
deduct(amount); // Optimistic UI update
try {
await action(); // The actual API call (which also deducts server-side)
return { success: true };
} catch (error) {
// Refund the optimistic deduction
await fetchCredits();
return { success: false, reason: 'error' };
}
};
const hasEnough = (amount: number) => credits !== null && credits >= amount;
return { credits, fetchCredits, spendCredits, hasEnough };
}
主要な UX パターン
ヘッダー/ナビゲーションでのクレジット表示:
- ユーザーのアバター/メニューの近くに現在の残高を表示する
- 表示形式:「42 credits」、またはコイン/トークンアイコンと「42」のみ
- ロード中で値が null の場合は、スケルトンまたは何も表示しません。ロード中に 0 を表示してはいけません。
残高不足の警告しきい値:
const LOW_CREDIT_THRESHOLD = 20; // Show gentle reminder
const CRITICAL_THRESHOLD = 10; // Show prominent warning
const ZERO_THRESHOLD = 0; // Block action, show buy modal
アクション前のクレジット確認: クレジットを消費するアクションの前に、クライアント側で確認します。
if (!hasEnough(CREDIT_COSTS.GENERATE)) {
openBuyCreditsModal();
return;
}
これにより、不要な API 呼び出しを防げます。サーバー側でも引き続き検証します。これは UX のための確認にすぎません。
フェーズ 6:UI コンポーネント
プロジェクトで既に使用されているデザインシステムに合わせて、次のコンポーネントを作成します。
1. クレジット残高の表示
- ヘッダー/ナビゲーションバーに表示します
- 操作後、リアルタイムで更新します
- クレジットが変動したときに控えめなパルスアニメーションを表示します
- クリックすると購入モーダルが開きます
2. クレジット購入モーダル/スライダー
- 価格付きのクレジットパックを表示します
- 最もお得なパックを強調表示します
- クレジットでできることを表示します:「100 クレジット = 約 20 回の生成 + 5 枚の画像」
- チェックアウトを作成してリダイレクトする CTA ボタンを表示します
3. クレジット残高低下時の警告
- 残高がしきい値を下回ると表示します
- LOW の場合は操作を妨げません(バナー/トースト)
- ZERO の場合は操作をブロックします(購入 CTA 付きモーダル)
- リマインダーとしてクレジットのコストを表示します
- 「クレジットを購入」プライマリ CTA と「気をつけます」閉じるボタン(クレジットが 0 より多い場合)
4. クレジットコストの表示
- 各アクションボタンの横にコストを表示します:「生成(5 クレジット)」
- クレジットが不足している場合、ボタンをグレー表示にします
- 無効なボタンのツールチップ:「あと X クレジット必要です」
5. チェックアウト成功ページ
- 「/checkout/success」ルート
- 残高が更新されるまで /api/user/credits をポーリングします(Webhook の反映には数秒かかる場合があります)
- 紙吹雪/お祝いの演出と新しい残高を表示します
- メインアプリに戻る CTA
6. 取引履歴(任意ですが推奨)
- クレジットの増減をすべてテーブル/リストで表示します
- 列:日付、アクション、金額(+/-)、変更後の残高
- 理由(購入、消費、ボーナス)で絞り込みます
- ユーザーの信頼につながります。クレジットの使い道を正確に確認できます
フェーズ 7:プロモコードシステム
引き換えエンドポイント
POST /api/credits/redeem
Body: { code: string }
Logic:
1. Get authenticated user
2. Find promo code by code string
3. Validate:
- Code exists
- Not expired (expires_at is null or > now)
- times_used < max_uses
- If email is set on code, must match user's email
4. Add credits to user
5. Increment times_used on promo code
6. Log transaction with reason='promo_code', metadata={ code }
7. Return { credits_added, new_balance }
管理者向けコード作成エンドポイント
POST /api/admin/promo-codes
Body: { credits_amount, max_uses?, email?, expires_in_days?, custom_code? }
Auth: Admin only
Logic:
1. Generate random 8-char code if no custom_code
2. Insert into promo_codes table
3. Return { code, credits_amount, expires_at }
プロモコードは、次の用途に不可欠です。
- ベータユーザーのオンボーディング
- インフルエンサー/パートナーへの配布
- カスタマーサポート(「問題が発生して申し訳ありません。50 クレジットをどうぞ」)
- マーケティングキャンペーン
フェーズ 8:管理者向けツール
クレジット追加エンドポイント
POST /api/admin/add-credits
Body: { email: string, amount: number, reason?: string }
Auth: Admin only (check admin secret header or admin role)
Logic:
1. Find user by email
2. Add credits
3. Log transaction with reason (default: 'admin_grant')
4. Return { email, credits_added, new_balance }
クレジット分析(credit_transactions から読み取り)
管理パネルに表示するのに役立つクエリ:
- 購入されたクレジットの合計(reason='purchase' の合計)
- 消費されたクレジットの合計(amount < 0 の合計)
- 未使用クレジット(全ユーザーの credits の合計)
- 収益(購入数 * 価格)
- 最もアクティブなユーザー(user_id でグループ化し、消費額を合計)
- アクション種別ごとの消費額(reason でグループ化)
フェーズ 9:すべてを連携する
すべての要素を作成したら、次のように連携します。
- 認証の同期:ユーザーがサインインしたら、クレジット残高を取得し、フロントエンドのストアに反映します
- すべての保護対象エンドポイント:適切なコストを指定したクレジットチェックのミドルウェアを追加します
- クレジットを消費するすべての UI アクション:クライアント側の hasEnough チェックとサーバー側のミドルウェアを追加します
- すべてのアクションの後:フロントエンドで楽観的に差し引き、API レスポンスで確定します
- Webhook ルート:決済プロバイダーに登録し、CLI ツールでテストします
- 成功ページ:ポーリングロジックを含む /checkout/success を作成します
検証チェックリスト
足場を作成したら、次のフローを確認します。
Flow 1: New User Signup
[ ] User signs up → gets FREE_CREDITS
[ ] Credit balance shows in UI
[ ] Transaction logged: reason='signup_bonus'
Flow 2: Spend Credits
[ ] User triggers action → credits deducted
[ ] UI updates optimistically
[ ] Server validates balance before processing
[ ] 402 returned if insufficient
[ ] Transaction logged with correct reason
Flow 3: Buy Credits
[ ] User clicks buy → redirected to checkout
[ ] Payment succeeds → webhook received
[ ] Webhook verified → credits added
[ ] Idempotent (double webhook doesn't double-credit)
[ ] UI refreshes with new balance
[ ] Transaction logged: reason='purchase'
Flow 4: Promo Code
[ ] User enters code → credits added
[ ] Code usage incremented
[ ] Expired/used codes rejected
[ ] Email-restricted codes enforced
Flow 5: Edge Cases
[ ] Insufficient credits → clear error, buy CTA shown
[ ] Concurrent requests → no negative balance (DB constraint or check)
[ ] Webhook arrives before redirect → still works
[ ] Network error during action → optimistic deduction rolled back
連携済みのフローと、手動テストが必要なフローをユーザーに伝えます。
重要な注意事項
- フロントエンドの残高を決して信用しないでください。 必ずサーバー側で検証してください。フロントエンドの残高は UX のためだけのものです。
- クレジットの変更をすべて記録してください。 正とするのは credits カラムではなく credit_transactions テーブルです。両者が食い違った場合は、取引履歴を優先します。
- Webhook の冪等性は必須です。 決済プロバイダーは再試行します。二重にクレジットを付与すると損失につながります。
- HTTP 402 が正しいステータスコードです。403 でも 400 でもありません。402 は「支払いが必要です」を意味します。
- シンプルに始めましょう。 まずは 1 つの価格で 1 つのクレジットパックを提供します。ユーザーが実際に何を購入するかデータが集まってから、段階を追加してください。