diff --git a/.github/pr-assets/session-cost/01-admin-session-trace-cost.png b/.github/pr-assets/session-cost/01-admin-session-trace-cost.png
new file mode 100644
index 0000000000..511c8633fc
Binary files /dev/null and b/.github/pr-assets/session-cost/01-admin-session-trace-cost.png differ
diff --git a/.github/pr-assets/session-cost/02-code-review-detail-cost.png b/.github/pr-assets/session-cost/02-code-review-detail-cost.png
new file mode 100644
index 0000000000..42e396c7dc
Binary files /dev/null and b/.github/pr-assets/session-cost/02-code-review-detail-cost.png differ
diff --git a/.github/pr-assets/session-cost/03-code-review-detail-fallback-cost.png b/.github/pr-assets/session-cost/03-code-review-detail-fallback-cost.png
new file mode 100644
index 0000000000..0a67cdeb15
Binary files /dev/null and b/.github/pr-assets/session-cost/03-code-review-detail-fallback-cost.png differ
diff --git a/apps/web/src/app/(app)/code-reviews/[reviewId]/CodeReviewDetailClient.tsx b/apps/web/src/app/(app)/code-reviews/[reviewId]/CodeReviewDetailClient.tsx
index 90e1ff5dc1..0d59249711 100644
--- a/apps/web/src/app/(app)/code-reviews/[reviewId]/CodeReviewDetailClient.tsx
+++ b/apps/web/src/app/(app)/code-reviews/[reviewId]/CodeReviewDetailClient.tsx
@@ -9,6 +9,10 @@ import { CouncilGovernancePanel } from '@/components/code-reviews/CouncilGoverna
import { formatTokenCount } from '@/lib/code-reviews/summary/usage-footer';
import { getCodeReviewJobsHref } from '@/lib/code-reviews/code-review-links';
import { getCodeReviewTerminalReasonCopy } from '@/lib/code-reviews/terminal-reason-copy';
+import {
+ formatSessionCost,
+ isRenderableSessionCost,
+} from '@/components/cloud-agent-next/session-cost-breakdown';
import { ExternalLink, GitPullRequest, Loader2, ArrowLeft, RotateCcw, Ban } from 'lucide-react';
import { getCodeReviewStatusIcon } from '@/components/code-reviews/code-review-status-icons';
import { useTRPC } from '@/lib/trpc/utils';
@@ -128,6 +132,10 @@ export function CodeReviewDetailClient({ reviewId }: CodeReviewDetailClientProps
const canCancel = isCancellableReviewStatus(status);
const prLabel = review.platform === 'gitlab' ? 'MR' : 'PR';
const jobsHref = getCodeReviewJobsHref(review.platform, review.owned_by_organization_id);
+ // Billing-derived session cost is authoritative when available; fall back to the
+ // orchestrator-accumulated review cost when the billing lookup has nothing (no
+ // linked session, an in-flight review, or a failed lookup).
+ const sessionCostMusd = data.sessionCostMusd ?? review.total_cost_musd;
const isSupersededCancellation =
status === 'cancelled' &&
(review.terminal_reason === 'superseded' ||
@@ -264,10 +272,10 @@ export function CodeReviewDetailClient({ reviewId }: CodeReviewDetailClientProps
)}
- {review.total_cost_musd != null && review.total_cost_musd > 0 && (
+ {sessionCostMusd != null && isRenderableSessionCost(sessionCostMusd / 1_000_000) && (
Cost
- ${(review.total_cost_musd / 1_000_000).toFixed(4)}
+ {formatSessionCost(sessionCostMusd / 1_000_000)}
)}
diff --git a/apps/web/src/app/admin/components/SessionTraceViewer.tsx b/apps/web/src/app/admin/components/SessionTraceViewer.tsx
index f60a2cfff5..02b3cc6f92 100644
--- a/apps/web/src/app/admin/components/SessionTraceViewer.tsx
+++ b/apps/web/src/app/admin/components/SessionTraceViewer.tsx
@@ -13,6 +13,10 @@ import { MessageErrorBoundary as V1MessageErrorBoundary } from '@/components/clo
import { convertToCloudMessages } from '@/components/cloud-agent/store/db-session-atoms';
import { MessageBubble as V2MessageBubble } from '@/components/cloud-agent-next/MessageBubble';
import { MessageErrorBoundary as V2MessageErrorBoundary } from '@/components/cloud-agent-next/MessageErrorBoundary';
+import {
+ formatSessionCost,
+ isRenderableSessionCost,
+} from '@/components/cloud-agent-next/session-cost-breakdown';
import { isNewSession } from '@/lib/cloud-agent/session-type';
import {
useAdminSessionTrace,
@@ -31,6 +35,7 @@ import {
Download,
Server,
Tag,
+ DollarSign,
} from 'lucide-react';
import type { CloudMessage, Message } from '@/components/cloud-agent/types';
import type { StoredMessage } from '@/components/cloud-agent-next/types';
@@ -403,6 +408,18 @@ export function SessionTraceViewer() {
)}
+ {sessionQuery.data.total_cost_microdollars != null &&
+ isRenderableSessionCost(sessionQuery.data.total_cost_microdollars / 1_000_000) && (
+
+
+
+ Cost:{' '}
+
+ {formatSessionCost(sessionQuery.data.total_cost_microdollars / 1_000_000)}
+
+
+
+ )}
{latestContainerInterval?.cloudflareInstanceId && (
diff --git a/apps/web/src/routers/admin-router.ts b/apps/web/src/routers/admin-router.ts
index 051291cc72..e7ba313896 100644
--- a/apps/web/src/routers/admin-router.ts
+++ b/apps/web/src/routers/admin-router.ts
@@ -2444,8 +2444,9 @@ export const adminRouter = createTRPCRouter({
return {
...session,
- // V1 doesn't have git_branch — null it out for a consistent shape
+ // V1 doesn't have git_branch or per-session cost — null them out for a consistent shape
git_branch: null,
+ total_cost_microdollars: null,
user: user
? {
id: user.id,
diff --git a/apps/web/src/routers/code-reviews-router.test.ts b/apps/web/src/routers/code-reviews-router.test.ts
index c849546f1c..6ac882f8f9 100644
--- a/apps/web/src/routers/code-reviews-router.test.ts
+++ b/apps/web/src/routers/code-reviews-router.test.ts
@@ -589,6 +589,66 @@ describe('codeReviewRouter.get role-gated ids', () => {
await db.delete(cloud_agent_code_reviews).where(eq(cloud_agent_code_reviews.id, review.id));
}
});
+
+ it('returns the billing-derived session cost for a terminal review', async () => {
+ const sessionId = `ses_get_cost_${crypto.randomUUID()}`;
+ const usageId = crypto.randomUUID();
+ const [review] = await db
+ .insert(cloud_agent_code_reviews)
+ .values({
+ owned_by_organization_id: null,
+ owned_by_user_id: ownerUser.id,
+ platform_integration_id: null,
+ repo_full_name: 'test-org/get-cost-repo',
+ pr_number: 2,
+ pr_url: 'https://github.com/test-org/get-cost-repo/pull/2',
+ pr_title: 'Get cost PR',
+ pr_author: 'octocat',
+ base_ref: 'main',
+ head_ref: 'feature/get-cost',
+ head_sha: 'sha-get-cost',
+ status: 'completed',
+ session_id: `agent_${crypto.randomUUID()}`,
+ cli_session_id: sessionId,
+ // The orchestrator never accumulated usage for this review, so the review's
+ // own total is 0 and the billing aggregate is the only cost available.
+ total_cost_musd: 0,
+ created_at: '2026-06-18T09:00:00.000Z',
+ completed_at: '2026-06-18T11:00:00.000Z',
+ })
+ .returning({ id: cloud_agent_code_reviews.id });
+
+ await db.insert(microdollar_usage).values({
+ id: usageId,
+ kilo_user_id: ownerUser.id,
+ cost: 1234,
+ input_tokens: 1000,
+ output_tokens: 100,
+ cache_write_tokens: 0,
+ cache_hit_tokens: 0,
+ created_at: '2026-06-18T10:00:00.000Z',
+ model: 'anthropic/claude-sonnet-4.6',
+ });
+ await db.insert(microdollar_usage_metadata).values({
+ id: usageId,
+ message_id: `msg_${usageId}`,
+ session_id: sessionId,
+ created_at: '2026-06-18T10:00:00.000Z',
+ });
+
+ try {
+ const ownerCaller = await createCallerForUser(ownerUser.id);
+ const ownerResult = await ownerCaller.codeReviews.get({ reviewId: review.id });
+ expect(ownerResult.success).toBe(true);
+ if (!ownerResult.success) throw new Error('expected owner get success');
+ expect(ownerResult.sessionCostMusd).toBe(1234);
+ expect(ownerResult.review.total_cost_musd).toBe(0);
+ } finally {
+ await db.delete(microdollar_usage_metadata).where(eq(microdollar_usage_metadata.id, usageId));
+ await db.delete(microdollar_usage).where(eq(microdollar_usage.id, usageId));
+ await db.delete(cloud_agent_code_reviews).where(eq(cloud_agent_code_reviews.id, review.id));
+ }
+ });
});
describe('personalReviewAgent.createManualReviewJob', () => {
diff --git a/apps/web/src/routers/code-reviews/code-reviews-router.ts b/apps/web/src/routers/code-reviews/code-reviews-router.ts
index d25d75a0a4..15867df49d 100644
--- a/apps/web/src/routers/code-reviews/code-reviews-router.ts
+++ b/apps/web/src/routers/code-reviews/code-reviews-router.ts
@@ -456,6 +456,7 @@ export const codeReviewRouter = createTRPCRouter({
},
attempts: visibleAttempts,
tokenUsage,
+ sessionCostMusd: billingUsage?.totalCostMusd ?? null,
});
} catch (error) {
if (error instanceof TRPCError) {