複数のプロダクトで Antigravity のエージェントを並行稼働させはじめてから、月末に Google Cloud と Anthropic の請求書を眺めるたびに「このコストはどのプロダクトが食ったのか、自信を持って答えられない」という気持ち悪さが残るようになりました。私は 2014 年からの個人開発で累計 5,000 万 DL のアプリ事業と、Dolice Labs の 4 サイト(Claude Lab・Gemini Lab・Antigravity Lab・Rork Lab)を並行運用していて、エージェントが触れる対象は壁紙アプリの AdMob 連動最適化から、各 Lab サイトの記事自動生成・Crashlytics トリアージまで横断します。共通のキャッシュ・共通の Gemma4 推論ノード・共通の Claude Agent SDK ジョブキューを使っているので、請求書を見ても「壁紙アプリ単独でいくら」「Claude Lab の記事生成でいくら」が分離できていませんでした。
最初は雑に「アプリ事業のほうが規模が大きいから 7 割はそっちだろう」と振り分けていたのですが、ある月に AdMob の eCPM 最適化エージェントが想定外のリトライ嵐を起こして、月予算の 71% を 1 つのテナントが吸い上げていた事故が起きました。按分が雑だと、原因の特定にもボトルネックの優先順位付けにも 1 日以上かかります。これを 2 時間以内に判定できる状態に持っていくために組み直したのが、本稿で整理する Showback アーキテクチャです。
Showback とは元々クラウド業界の用語で「実行コストをチャージはしないが、テナント別に見える化する」設計を指します。Chargeback(実際に課金まで行う)よりも一段ライトで、まずは「誰が・いつ・何に使ったか」を全部の経路で計測してダッシュボードに出すところを徹底するアプローチです。個人開発レベルでも、複数プロダクトを横断するエージェント運用ではこれが効きます。
Tenant ID をどう運ぶか — Context Propagation の設計
Showback の出発点は「すべてのコスト発生イベントに、誰のためのコストかというラベル(Tenant ID)が必ず付いている」状態を作ることです。これが抜け落ちると、月末の集計で「unattributed: ¥98,400」のような不気味な余りが残り続けて、せっかくの按分の信頼性が崩れます。
私が採用しているのは TypeScript の AsyncLocalStorage を使った Context Propagation です。エージェントの入口(HTTP リクエスト・Cron トリガー・キュー consumer のいずれか)でテナントを確定させ、そこから先のすべての非同期処理に、引数を増やすことなく Tenant ID を運びます。Next.js Route Handler から呼び出すバージョンは以下のような形になります。
// src/lib/tenant-context.ts
import { AsyncLocalStorage } from "node:async_hooks";
export type TenantContext = {
tenantId: string; // 例: "wallpaper-zen-ios" / "claudelab-net" / "internal-cron"
productGroup: "app" | "lab" | "internal";
invocationId: string; // 1 リクエスト = 1 ID
budgetCapJpy?: number; // 月次キャップ(任意)
};
const storage = new AsyncLocalStorage<TenantContext>();
export function runWithTenant<T>(ctx: TenantContext, fn: () => Promise<T>) {
return storage.run(ctx, fn);
}
export function currentTenant(): TenantContext {
const ctx = storage.getStore();
if (!ctx) {
// 「unattributed」を生まないために、未設定はエラーで弾く
throw new Error("tenant context missing — runWithTenant() で必ず包んでください");
}
return ctx;
}設計上のポイントは 3 つあります。第 1 に、未設定をデフォルト値で許容しないこと。「unknown テナント」「default テナント」のような逃げ道を作ると、ある日気づくと 4 割のコストがそこに溜まります。私は最初の 1 週間、ここでよくエラーを踏みましたが、結果的に未計測パスが綺麗に洗い出されたので、デフォルト禁止は本番運用の前提として強く推奨します。
第 2 に、invocationId を必ず ULID か UUIDv7 で発行すること。後述する Showback Ledger でユニーク制約をかけて二重計上を防ぐためです。第 3 に、productGroup をエージェント側で分類しておくこと。私の場合は app / lab / internal の 3 区分にしておくと、月次レポートの軸を切り替えるときに楽になります。
コスト計測の単位 — トークン・リクエスト・副作用の 3 軸
Antigravity エージェントの実行コストは、単一の単位で測ろうとすると必ずどこかが歪みます。私は次の 3 軸で計測しています。
第 1 軸はトークン課金で、Gemini 3 Pro Thinking のような LLM 呼び出し本体のコストです。Anthropic と Google の単価は input / output で異なるので、課金単価表をテーブルに持って「呼び出し時の単価 × トークン数」を都度記録します。エージェントの再試行を含めて全部を残すので、Tier 1 へのフォールバック中の冗長な呼び出しも漏れません。
第 2 軸はリクエスト課金で、ツール側 API の都度課金(Apple Search Ads API・Crashlytics Reporting API・Stripe API など)と、Cloudflare Workers の Invocation 課金です。1 リクエスト ¥0.001 のような単位で積み上がるので、按分しないと「誰が呼んだか」が消えがちです。
第 3 軸は副作用で、エージェントが本番に書き込んだ結果のコスト(広告配信費・通知送信費・SMS / メール送信費)です。これは LLM コストの 3〜10 倍規模になることが普通なので、Showback でいちばん重要な軸です。たとえば AdMob mediation の A/B テスト用に内製のキーワード入札エージェントを動かすと、エージェント側のトークンコストは月 ¥4,200 ですが、結果として動く広告予算は月 ¥180,000 を超えます。後者を計測しないと「LLM 安いから問題なし」と判断ミスをします。
実装は薄いラッパーで十分です。Anthropic SDK と Google AI SDK を「テナント付き」で呼ぶ薄い層を作るだけで、コードの大半は元のまま動きます。
// src/lib/cost-meter.ts
import { currentTenant } from "./tenant-context";
import { writeLedgerRow } from "./ledger";
type Pricing = { inputPer1k: number; outputPer1k: number; currency: "JPY" };
const PRICING: Record<string, Pricing> = {
"gemini-3-pro-thinking": { inputPer1k: 0.42, outputPer1k: 2.10, currency: "JPY" },
"claude-opus-4-6": { inputPer1k: 2.25, outputPer1k: 11.25, currency: "JPY" },
"gemma4-27b-onprem": { inputPer1k: 0.00, outputPer1k: 0.00, currency: "JPY" }, // 電気代は別途
};
export async function meterLlmCall<T>(opts: {
model: keyof typeof PRICING;
inputTokens: number;
outputTokens: number;
result: T;
}): Promise<T> {
const tenant = currentTenant();
const p = PRICING[opts.model];
const costJpy =
(opts.inputTokens / 1000) * p.inputPer1k +
(opts.outputTokens / 1000) * p.outputPer1k;
await writeLedgerRow({
tenantId: tenant.tenantId,
productGroup: tenant.productGroup,
invocationId: tenant.invocationId,
axis: "tokens",
model: opts.model,
units: opts.inputTokens + opts.outputTokens,
costJpy,
occurredAt: new Date().toISOString(),
});
return opts.result;
}副作用軸も同じ形のラッパーで実装します。axis: "side_effect" を別行で書き込んで、ledger は 3 軸すべてを同じテーブルに溜めます。同一テーブルにしておくと、後述の月次レポートで SUM(cost_jpy) GROUP BY tenant, axis の素直なクエリでまとめられて運用が楽になります。