Skip to content

Commit 6d2e60d

Browse files
[codex] Add Carbon fallback for CLI ads (#541)
Co-authored-by: James Grugett <jahooma@gmail.com>
1 parent 585260b commit 6d2e60d

3 files changed

Lines changed: 64 additions & 42 deletions

File tree

cli/src/chat.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,11 @@ export const Chat = ({
174174
})
175175
const hasSubscription = subscriptionData?.hasSubscription ?? false
176176

177-
const { adData, recordImpression } = useGravityAd({ enabled: IS_FREEBUFF || !hasSubscription })
177+
const { adData, recordImpression } = useGravityAd({
178+
enabled: IS_FREEBUFF || !hasSubscription,
179+
provider: 'gravity',
180+
fallbackProvider: 'carbon',
181+
})
178182

179183
// Set initial mode from CLI flag on mount
180184
useEffect(() => {

cli/src/components/waiting-room-screen.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,12 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
8484
// Always enable ads in the waiting room — this is where monetization lives.
8585
// forceStart bypasses the "wait for first user message" gate inside the hook,
8686
// which would otherwise block ads here since no conversation exists yet.
87-
// Uses Carbon (BuySellAds); in-chat ads still use the Gravity default.
87+
// Try Gravity first, then fall back to Carbon when Gravity doesn't fill.
8888
const { adData, recordImpression } = useGravityAd({
8989
enabled: true,
9090
forceStart: true,
91-
provider: 'carbon',
91+
provider: 'gravity',
92+
fallbackProvider: 'carbon',
9293
})
9394

9495
useFreebuffCtrlCExit()

cli/src/hooks/use-gravity-ad.ts

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,15 @@ export const useGravityAd = (options?: {
108108
/** Skip the "wait for first user message" gate. Used by the freebuff
109109
* waiting room, which has no conversation but still needs ads. */
110110
forceStart?: boolean
111-
/** Which ad network to query. Defaults to Gravity. */
111+
/** Primary ad network to query. Defaults to Gravity. */
112112
provider?: AdProvider
113+
/** Backup ad network to try when the primary returns no fill or errors. */
114+
fallbackProvider?: AdProvider
113115
}): GravityAdState => {
114116
const enabled = options?.enabled ?? true
115117
const forceStart = options?.forceStart ?? false
116118
const provider: AdProvider = options?.provider ?? 'gravity'
119+
const fallbackProvider = options?.fallbackProvider
117120
const [ad, setAd] = useState<AdResponse | null>(null)
118121
const [adData, setAdData] = useState<AdData | null>(null)
119122
const [isLoading, setIsLoading] = useState(false)
@@ -278,49 +281,63 @@ export const useGravityAd = (options?: {
278281
}
279282
}
280283

281-
try {
282-
const response = await fetch(`${WEBSITE_URL}/api/v1/ads`, {
283-
method: 'POST',
284-
headers: {
285-
'Content-Type': 'application/json',
286-
Authorization: `Bearer ${authToken}`,
287-
},
288-
body: JSON.stringify({
289-
provider,
290-
messages: adMessages,
291-
sessionId: useChatStore.getState().chatSessionId,
292-
device: getDeviceInfo(),
293-
// Carbon requires a real browser-ish useragent for targeting/fraud
294-
// detection. Gravity ignores it. We source one centrally so every
295-
// provider that needs it sees the same value.
296-
userAgent: getAdUserAgent(),
297-
}),
298-
})
284+
const providersToTry =
285+
fallbackProvider && fallbackProvider !== provider
286+
? [provider, fallbackProvider]
287+
: [provider]
299288

300-
if (!response.ok) {
301-
logger.warn(
302-
{ provider, status: response.status, response: await response.json() },
303-
'[ads] Web API returned error',
304-
)
305-
return null
306-
}
289+
for (const providerToTry of providersToTry) {
290+
try {
291+
const response = await fetch(`${WEBSITE_URL}/api/v1/ads`, {
292+
method: 'POST',
293+
headers: {
294+
'Content-Type': 'application/json',
295+
Authorization: `Bearer ${authToken}`,
296+
},
297+
body: JSON.stringify({
298+
provider: providerToTry,
299+
messages: adMessages,
300+
sessionId: useChatStore.getState().chatSessionId,
301+
device: getDeviceInfo(),
302+
// Carbon requires a real browser-ish useragent for targeting/fraud
303+
// detection. Gravity ignores it. We source one centrally so every
304+
// provider that needs it sees the same value.
305+
userAgent: getAdUserAgent(),
306+
}),
307+
})
307308

308-
const data = await response.json()
309-
const variant = data.variant ?? 'banner'
309+
if (!response.ok) {
310+
logger.warn(
311+
{
312+
provider: providerToTry,
313+
status: response.status,
314+
response: await response.json(),
315+
},
316+
'[ads] Web API returned error',
317+
)
318+
continue
319+
}
310320

311-
if (variant === 'choice' && Array.isArray(data.ads) && data.ads.length > 0) {
312-
return { variant: 'choice', ads: data.ads as AdResponse[] }
313-
}
321+
const data = await response.json()
322+
const variant = data.variant ?? 'banner'
314323

315-
if (data.ad) {
316-
return { variant: 'banner', ad: data.ad as AdResponse }
317-
}
324+
if (
325+
variant === 'choice' &&
326+
Array.isArray(data.ads) &&
327+
data.ads.length > 0
328+
) {
329+
return { variant: 'choice', ads: data.ads as AdResponse[] }
330+
}
318331

319-
return null
320-
} catch (err) {
321-
logger.error({ err }, '[ads] Failed to fetch ad')
322-
return null
332+
if (data.ad) {
333+
return { variant: 'banner', ad: data.ad as AdResponse }
334+
}
335+
} catch (err) {
336+
logger.error({ err, provider: providerToTry }, '[ads] Failed to fetch ad')
337+
}
323338
}
339+
340+
return null
324341
}
325342

326343
// Update tick function (uses ref to avoid useCallback dependency issues)
@@ -413,7 +430,7 @@ export const useGravityAd = (options?: {
413430
clearInterval(id)
414431
ctrlRef.current.intervalId = null
415432
}
416-
}, [shouldStart, shouldHideAds])
433+
}, [shouldStart, shouldHideAds, provider, fallbackProvider])
417434

418435
// Don't return ad when ads should be hidden
419436
const visible = shouldStart && !shouldHideAds

0 commit comments

Comments
 (0)