@@ -6,6 +6,7 @@ import { recordUsage } from '@/lib/billing/core/usage-log'
66import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
77import { checkInternalApiKey } from '@/lib/copilot/request/http'
88import { isBillingEnabled } from '@/lib/core/config/feature-flags'
9+ import { type AtomicClaimResult , billingIdempotency } from '@/lib/core/idempotency/service'
910import { generateRequestId } from '@/lib/core/utils/request'
1011
1112const logger = createLogger ( 'BillingUpdateCostAPI' )
@@ -19,6 +20,7 @@ const UpdateCostSchema = z.object({
1920 source : z
2021 . enum ( [ 'copilot' , 'workspace-chat' , 'mcp_copilot' , 'mothership_block' ] )
2122 . default ( 'copilot' ) ,
23+ idempotencyKey : z . string ( ) . min ( 1 ) . optional ( ) ,
2224} )
2325
2426/**
@@ -28,6 +30,7 @@ const UpdateCostSchema = z.object({
2830export async function POST ( req : NextRequest ) {
2931 const requestId = generateRequestId ( )
3032 const startTime = Date . now ( )
33+ let claim : AtomicClaimResult | null = null
3134
3235 try {
3336 logger . info ( `[${ requestId } ] Update cost request started` )
@@ -75,9 +78,30 @@ export async function POST(req: NextRequest) {
7578 )
7679 }
7780
78- const { userId, cost, model, inputTokens, outputTokens, source } = validation . data
81+ const { userId, cost, model, inputTokens, outputTokens, source, idempotencyKey } =
82+ validation . data
7983 const isMcp = source === 'mcp_copilot'
8084
85+ claim = idempotencyKey
86+ ? await billingIdempotency . atomicallyClaim ( 'update-cost' , idempotencyKey )
87+ : null
88+
89+ if ( claim && ! claim . claimed ) {
90+ logger . warn ( `[${ requestId } ] Duplicate billing update rejected` , {
91+ idempotencyKey,
92+ userId,
93+ source,
94+ } )
95+ return NextResponse . json (
96+ {
97+ success : false ,
98+ error : 'Duplicate request: idempotency key already processed' ,
99+ requestId,
100+ } ,
101+ { status : 409 }
102+ )
103+ }
104+
81105 logger . info ( `[${ requestId } ] Processing cost update` , {
82106 userId,
83107 cost,
@@ -149,6 +173,17 @@ export async function POST(req: NextRequest) {
149173 duration,
150174 } )
151175
176+ if ( claim ?. claimed ) {
177+ await billingIdempotency
178+ . release ( claim . normalizedKey , claim . storageMethod )
179+ . catch ( ( releaseErr ) => {
180+ logger . warn ( `[${ requestId } ] Failed to release idempotency claim` , {
181+ error : releaseErr instanceof Error ? releaseErr . message : String ( releaseErr ) ,
182+ normalizedKey : claim ?. normalizedKey ,
183+ } )
184+ } )
185+ }
186+
152187 return NextResponse . json (
153188 {
154189 success : false ,
0 commit comments